Completion block for popViewController

68,080

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;
}
Share:
68,080
Ben Packard
Author by

Ben Packard

iOS developer and program manager located in Washington, DC.

Updated on October 22, 2021

Comments

  • Ben Packard
    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 for popViewController?

    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 an UIView 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
    Ben Packard over 11 years
    I 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
    mattjgalloway over 11 years
    It's only very new on the dismissViewController. Maybe it'll come to popViewController. File a radar :-).
  • Ben Packard
    Ben Packard over 11 years
    Sure - except then you have to handle all the cases where the view is disappearing for some other reason.
  • mattjgalloway
    mattjgalloway over 11 years
    Seriously though, do file a radar. It's more likely to make it in if people ask for it.
  • Ben Packard
    Ben Packard over 11 years
    I just tried on bugreport.apple.com - are feature requests some place else?
  • mattjgalloway
    mattjgalloway over 11 years
    That's the right place to ask for it. There's an option for the classification to be 'Feature'.
  • Jason Coco
    Jason Coco over 11 years
    This 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
    rdelmar over 11 years
    @BenPackard, yes, and the same is true for putting it in viewDidAppear in the answer you accepted.
  • mattjgalloway
    mattjgalloway over 11 years
    Yep that's true, you could do it in that as well, good point.
  • Ben Packard
    Ben Packard over 11 years
    Similar limitations though right (e.g. having to test if the animation should fire based on some logic that prevents it in other cases)?
  • spstanley
    spstanley about 10 years
    I really like this solution, I'm going to try it with a category and an associated object.
  • k06a
    k06a over 9 years
    @spstanley you need to publish this pod :)
  • Arbitur
    Arbitur over 9 years
    I 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
    moger777 about 9 years
    Does 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
    moger777 about 9 years
    OK, 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
    fabb about 9 years
    Does not work for me on iOS 7, the completion block is called immediately.
  • mattjgalloway
    mattjgalloway almost 9 years
    Yeh, 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
    stuckj almost 9 years
    Yeah, 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
    user3344977 almost 9 years
    This works for me when pushing a view controller but not when popping on iOS 8. My completion block never fires.
  • Julian F. Weinert
    Julian F. Weinert almost 9 years
    Interesting. For me, in iOS 8.4, the completion block does fire, but approx. half the way down the animation.
  • Julian F. Weinert
    Julian F. Weinert almost 9 years
    For 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
    durazno about 8 years
    THIS DOES NOT WORK
  • pronebird
    pronebird almost 7 years
    @rshev why on next runloop?
  • rshev
    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
    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
    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
    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
    Dennis almost 6 years
    when you add an alertview or some animation in viewdidload, it will not work immediately
  • Bogdan Razvan
    Bogdan Razvan over 5 years
    @Arbitur completion block is indeed called after calling popViewController or pushViewController, but if you check what the topViewController is right afterwards, you will notice it is still the old one, just like pop or push never happened...
  • Sean
    Sean about 5 years
    The 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
    Arbitur about 5 years
    @BogdanRazvan right afterwards what? Does your completion closure get called once the animation is complete?
  • kball
    kball about 5 years
    Prematurely added +1. Should be -1. This does not work consistently, which is kind of worse than it not working at all.
  • Yogesh Patel
    Yogesh Patel almost 5 years
    milions of thanks working for me please note use pushviewcontroller either sometimes it not working :)
  • CyberMew
    CyberMew over 4 years
    This does not work if I am using navigationController?.popToRootViewController; it works if I am using navigationController?.popViewController (at least on iOS 13.1).
  • Iliyan Kafedzhiev
    Iliyan Kafedzhiev about 4 years
    Incorrect! 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
    Iliyan Kafedzhiev about 4 years
  • leviathan
    leviathan about 4 years
    Any particular reason why you're calling the completion() async?
  • rshev
    rshev about 4 years
    when animating with coordinator completion is never executed on the same runloop. this guarantees completion never runs on the same runloop when not animating. it's better to not have this kind of inconsistency.
  • Bogdan Razvan
    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
    Oliver Pearmain over 3 years
    While this does work the issue is that it restricts other use of UINavigationControllerDelegate
  • Bawenang Rukmoko Pardian Putra
    Bawenang Rukmoko Pardian Putra over 2 years
    @HotJard you forgot to add @escaping on the completion handler.
  • Leon
    Leon over 2 years
    This is not Swift 5, if let where was removed in Swift 3! Also, transitionCoordinator() is wrong, it's a property not a method.
  • HotJard
    HotJard over 2 years
    @Leon thx, updated