Completion block for popViewController
Solution 1
I know an answer has been accepted over two years ago, however this answer is incomplete.
There is no way to do what you're wanting out-of-the-box
This is technically correct because the UINavigationController
API doesn't offer any options for this. However by using the CoreAnimation framework it's possible to add a completion block to the underlying animation:
[CATransaction begin];
[CATransaction setCompletionBlock:^{
// handle completion here
}];
[self.navigationController popViewControllerAnimated:YES];
[CATransaction commit];
The completion block will be called as soon as the animation used by popViewControllerAnimated:
ends. This functionality has been available since iOS 4.
Solution 2
Swift 5 version - works like a charm. Based on this answer
extension UINavigationController {
func pushViewController(viewController: UIViewController, animated: Bool, completion: @escaping () -> Void) {
pushViewController(viewController, animated: animated)
if animated, let coordinator = transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion()
}
} else {
completion()
}
}
func popViewController(animated: Bool, completion: @escaping () -> Void) {
popViewController(animated: animated)
if animated, let coordinator = transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion()
}
} else {
completion()
}
}
}
Solution 3
I made a Swift
version with extensions with @JorisKluivers answer.
This will call a completion closure after the animation is done for both push
and pop
.
extension UINavigationController {
func popViewControllerWithHandler(completion: ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.popViewControllerAnimated(true)
CATransaction.commit()
}
func pushViewController(viewController: UIViewController, completion: ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.pushViewController(viewController, animated: true)
CATransaction.commit()
}
}
Solution 4
SWIFT 4.1
extension UINavigationController {
func pushToViewController(_ viewController: UIViewController, animated:Bool = true, completion: @escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.pushViewController(viewController, animated: animated)
CATransaction.commit()
}
func popViewController(animated:Bool = true, completion: @escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.popViewController(animated: animated)
CATransaction.commit()
}
func popToViewController(_ viewController: UIViewController, animated:Bool = true, completion: @escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.popToViewController(viewController, animated: animated)
CATransaction.commit()
}
func popToRootViewController(animated:Bool = true, completion: @escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
self.popToRootViewController(animated: animated)
CATransaction.commit()
}
}
Solution 5
I had the same issue. And because I had to use it in multiple occasions, and within chains of completion blocks, I created this generic solution in an UINavigationController subclass:
- (void) navigationController:(UINavigationController *) navigationController didShowViewController:(UIViewController *) viewController animated:(BOOL) animated {
if (_completion) {
dispatch_async(dispatch_get_main_queue(),
^{
_completion();
_completion = nil;
});
}
}
- (UIViewController *) popViewControllerAnimated:(BOOL) animated completion:(void (^)()) completion {
_completion = completion;
return [super popViewControllerAnimated:animated];
}
Assuming
@interface NavigationController : UINavigationController <UINavigationControllerDelegate>
and
@implementation NavigationController {
void (^_completion)();
}
and
- (id) initWithRootViewController:(UIViewController *) rootViewController {
self = [super initWithRootViewController:rootViewController];
if (self) {
self.delegate = self;
}
return self;
}
Ben Packard
iOS developer and program manager located in Washington, DC.
Updated on October 22, 2021Comments
-
Ben Packard over 2 years
When dismissing a modal view controller using
dismissViewController
, there is the option to provide a completion block. Is there a similar equivalent forpopViewController
?The completion argument is quite handy. For instance, I can use it to hold off removing a row from a tableview until the modal is off screen, letting the user see the row animation. When returning from a pushed view controller, I would like the same opportunity.
I have tried placing
popViewController
in anUIView
animation block, where I do have access to a completion block. However, this produces some unwanted side effects on the view being popped to.If there is no such method available, what are some workarounds?
-
Ben Packard over 11 yearsI attempted this. I was storing an array of 'deleted row indexes' and whenever the view appears, checking to see if anything needs to be removed. It quickly grew unwieldy but I might give it another shot. I wonder why Apple provide it for one transition but not the other?
-
mattjgalloway over 11 yearsIt's only very new on the
dismissViewController
. Maybe it'll come topopViewController
. File a radar :-). -
Ben Packard over 11 yearsSure - except then you have to handle all the cases where the view is disappearing for some other reason.
-
mattjgalloway over 11 yearsSeriously though, do file a radar. It's more likely to make it in if people ask for it.
-
Ben Packard over 11 yearsI just tried on bugreport.apple.com - are feature requests some place else?
-
mattjgalloway over 11 yearsThat's the right place to ask for it. There's an option for the classification to be 'Feature'.
-
Jason Coco over 11 yearsThis answer is not completely correct. While you can't set the new-style block like on
-dismissViewController:animated:completionBlock:
, but you can get the animation through the navigation controller's delegate. After the animation is complete,-navigationController:didShowViewController:animated:
will be called on the delegate and you can do whatever you'd need right there. -
rdelmar over 11 years@BenPackard, yes, and the same is true for putting it in viewDidAppear in the answer you accepted.
-
mattjgalloway over 11 yearsYep that's true, you could do it in that as well, good point.
-
Ben Packard over 11 yearsSimilar limitations though right (e.g. having to test if the animation should fire based on some logic that prevents it in other cases)?
-
spstanley about 10 yearsI really like this solution, I'm going to try it with a category and an associated object.
-
k06a over 9 years@spstanley you need to publish this pod :)
-
Arbitur over 9 yearsI put this in an extension of UINavigationController in Swift:
extension UINavigationController { func popViewControllerWithHandler(handler: ()->()) { CATransaction.begin() CATransaction.setCompletionBlock(handler) self.popViewControllerAnimated(true) CATransaction.commit() } }
-
moger777 about 9 yearsDoes not seem to work for me, when I do completionHandler on dismissViewController, the view that was presenting it is part of view hierarchy. When I do the same with the CATransaction, I get a warning that the view is not part of the view hierarchy.
-
moger777 about 9 yearsOK, looks like your works if you reverse the begin and completion block. Sorry about the down vote but stack overflow won't let me change :(
-
fabb about 9 yearsDoes not work for me on iOS 7, the completion block is called immediately.
-
mattjgalloway almost 9 yearsYeh, while this may work, it's not exactly defined behaviour. You can't rely on this working.
popViewControllerAnimated:
may dispatch away to somewhere else for example which would stop this from working. It's a great shout though, and if it works on iOS 8, then cool! -
stuckj almost 9 yearsYeah, this seemed like it would be awesome, but it doesn't appear to work (at least on iOS 8). The completion block is getting called immediately. Likely because of the mixture of core animations with UIView style animations.
-
user3344977 almost 9 yearsThis works for me when pushing a view controller but not when popping on iOS 8. My completion block never fires.
-
Julian F. Weinert almost 9 yearsInteresting. For me, in iOS 8.4, the completion block does fire, but approx. half the way down the animation.
-
Julian F. Weinert almost 9 yearsFor me, in iOS 8.4, written in ObjC the block fires half the way down the animation. Does this really fire in the right moment if written in Swift (8.4)?
-
durazno about 8 yearsTHIS DOES NOT WORK
-
pronebird almost 7 years@rshev why on next runloop?
-
rshev almost 7 years@Andy from what I remember experimenting with this, something hadn't been propagated yet at that point. Try experimenting with it, love to hear how it works for you.
-
pronebird almost 7 years@rshev I think I had it the same way before, I have to double check. Current tests run fine.
-
Lance Samaria almost 7 years@HotJard hello, where would I call this a when popping? I don't want it to trigger until the view has been officially unloaded off the screen by either a right swipe to dismiss or pressing the back button. ViewWillDisappear and ViewDidDisappear gets trigged whenever I switch the views (pressing a diff tabbar button). Any suggestions?
-
HotJard almost 7 years@LanceSamaria I suggest to use viewDidDisappear. Check if navbar is available, if not – it's not shown in navbar, so it was popped. if (self.navigationController == nil) { trigger your action }
-
Dennis almost 6 yearswhen you add an alertview or some animation in viewdidload, it will not work immediately
-
Bogdan Razvan over 5 years@Arbitur completion block is indeed called after calling
popViewController
orpushViewController
, but if you check what the topViewController is right afterwards, you will notice it is still the old one, just likepop
orpush
never happened... -
Sean about 5 yearsThe accepted answer appears to work in my dev environment with all the emulators/devices I have, but I still get bug reported from production users. Not sure if this will solve the production issue, but let me upvote it just so someone may try it if getting the same issue from the accepted answer.
-
Arbitur about 5 years@BogdanRazvan right afterwards what? Does your completion closure get called once the animation is complete?
-
kball about 5 yearsPrematurely added +1. Should be -1. This does not work consistently, which is kind of worse than it not working at all.
-
Yogesh Patel almost 5 yearsmilions of thanks working for me please note use pushviewcontroller either sometimes it not working :)
-
CyberMew over 4 yearsThis does not work if I am using
navigationController?.popToRootViewController
; it works if I am usingnavigationController?.popViewController
(at least on iOS 13.1). -
Iliyan Kafedzhiev about 4 yearsIncorrect! It is executed even before viewDidDissapear of the viewController that is poping and can cause a lot of 'hidden' problems if you use it.
-
Iliyan Kafedzhiev about 4 yearsSwift version -> stackoverflow.com/a/60090678/4010725
-
leviathan about 4 yearsAny particular reason why you're calling the
completion()
async? -
rshev about 4 yearswhen animating with coordinator
completion
is never executed on the same runloop. this guaranteescompletion
never runs on the same runloop when not animating. it's better to not have this kind of inconsistency. -
Bogdan Razvan over 3 years@Arbitur right after the animation is complete. Yes, the completion closure gets called once the animation is complete, but the topViewController is still the old one, just as it was not yet popped.
-
Oliver Pearmain over 3 yearsWhile this does work the issue is that it restricts other use of
UINavigationControllerDelegate
-
Bawenang Rukmoko Pardian Putra over 2 years@HotJard you forgot to add
@escaping
on the completion handler. -
Leon over 2 yearsThis is not Swift 5,
if let where
was removed in Swift 3! Also,transitionCoordinator()
is wrong, it's a property not a method. -
HotJard over 2 years@Leon thx, updated