137

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?

mattjgalloway
  • 34,792
  • 12
  • 100
  • 110
Ben Packard
  • 26,102
  • 25
  • 102
  • 183

21 Answers21

215

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.

Joris Kluivers
  • 11,894
  • 2
  • 48
  • 47
  • 8
    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() } }` – Arbitur Jan 30 '15 at 09:07
  • 1
    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 Feb 10 '15 at 17:24
  • 1
    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 :( – moger777 Feb 10 '15 at 17:30
  • Does not work for me on iOS 7, the completion block is called immediately. – fabb Apr 22 '15 at 09:59
  • 1
    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! – mattjgalloway May 13 '15 at 21:01
  • 7
    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. – stuckj Jun 03 '15 at 21:30
  • This works for me when pushing a view controller but not when popping on iOS 8. My completion block never fires. – user3344977 Jul 02 '15 at 03:05
  • Interesting. For me, in iOS 8.4, the completion block does fire, but approx. half the way down the animation. – Julian F. Weinert Jul 29 '15 at 19:23
  • 9
    THIS DOES NOT WORK – durazno Apr 09 '16 at 12:29
  • when you add an alertview or some animation in viewdidload, it will not work immediately – Dennis Jul 12 '18 at 12:20
  • 3
    Prematurely added +1. Should be -1. This does not work consistently, which is kind of worse than it not working at all. – kball Mar 20 '19 at 21:52
  • milions of thanks working for me please note use pushviewcontroller either sometimes it not working :) – Yogesh Patel May 31 '19 at 12:13
  • 1
    This does not work if I am using `navigationController?.popToRootViewController`; it works if I am using `navigationController?.popViewController` (at least on iOS 13.1). – CyberMew Oct 21 '19 at 09:45
  • 1
    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 Feb 06 '20 at 08:14
  • sometimes it doesn't work – Gargo May 22 '23 at 07:54
78

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()
        }
    }
}
HotJard
  • 4,598
  • 2
  • 36
  • 36
  • @rshev why on next runloop? – pronebird May 20 '17 at 15:18
  • @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. – rshev May 22 '17 at 15:37
  • @rshev I think I had it the same way before, I have to double check. Current tests run fine. – pronebird May 22 '17 at 15:54
  • @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? – Lance Samaria Jun 12 '17 at 20:47
  • 1
    @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 } – HotJard Jun 13 '17 at 13:10
  • @HotJard you forgot to add `@escaping` on the completion handler. – Bawenang Rukmoko Pardian Putra Aug 25 '21 at 02:54
  • This is not Swift 5, `if let where` was removed in Swift 3! Also, `transitionCoordinator()` is wrong, it's a property not a method. – Leon Oct 18 '21 at 08:03
41

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()
    }
}
Joris Kluivers
  • 11,894
  • 2
  • 48
  • 47
Arbitur
  • 38,684
  • 22
  • 91
  • 128
  • 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)? – Julian F. Weinert Jul 29 '15 at 19:26
  • @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... – Bogdan Razvan Jan 29 '19 at 09:18
  • @BogdanRazvan right afterwards what? Does your completion closure get called once the animation is complete? – Arbitur Feb 18 '19 at 15:43
  • @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. – Bogdan Razvan Sep 18 '20 at 11:08
  • sometimes it doesn't work – Gargo May 22 '23 at 07:54
20

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()
}
}
Abdul Rehman
  • 2,386
  • 1
  • 20
  • 34
Muhammad Waqas
  • 904
  • 2
  • 10
  • 21
17

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;
}
Jos Jong
  • 181
  • 1
  • 4
17

Working with or without animation properly, and also includes popToRootViewController:

 // updated for Swift 3.0
extension UINavigationController {

  private func doAfterAnimatingTransition(animated: Bool, completion: @escaping (() -> Void)) {
    if let coordinator = transitionCoordinator, animated {
      coordinator.animate(alongsideTransition: nil, completion: { _ in
        completion()
      })
    } else {
      DispatchQueue.main.async {
        completion()
      }
    }
  }

  func pushViewController(viewController: UIViewController, animated: Bool, completion: @escaping (() ->     Void)) {
    pushViewController(viewController, animated: animated)
    doAfterAnimatingTransition(animated: animated, completion: completion)
  }

  func popViewController(animated: Bool, completion: @escaping (() -> Void)) {
    popViewController(animated: animated)
    doAfterAnimatingTransition(animated: animated, completion: completion)
  }

  func popToRootViewController(animated: Bool, completion: @escaping (() -> Void)) {
    popToRootViewController(animated: animated)
    doAfterAnimatingTransition(animated: animated, completion: completion)
  }
}
allaire
  • 5,995
  • 3
  • 41
  • 56
rshev
  • 4,086
  • 1
  • 23
  • 32
  • 1
    Any particular reason why you're calling the `completion()` async? – leviathan Mar 09 '20 at 09:04
  • 2
    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. – rshev Mar 10 '20 at 10:12
17

Based on @HotJard's answer, when all you want is just a couple of lines of code. Quick and Easy.

Swift 4:

_ = self.navigationController?.popViewController(animated: true)
self.navigationController?.transitionCoordinator.animate(alongsideTransition: nil) { _ in
    doWhatIWantAfterContollerHasPopped()
}
Fattie
  • 27,874
  • 70
  • 431
  • 719
Vitalii
  • 4,267
  • 1
  • 40
  • 45
15

There is no way to do what you're wanting out-of-the-box. i.e. there is no method with a completion block for popping a view controller from a nav stack.

What I would do is put the logic in viewDidAppear. That will be called when the view has finished coming on screen. It'll be called for all different scenarios of the view controller appearing, but that should be fine.

Or you could use the UINavigationControllerDelegate method navigationController:didShowViewController:animated: to do a similar thing. This is called when the navigation controller has finished pushing or popping a view controller.

mattjgalloway
  • 34,792
  • 12
  • 100
  • 110
  • 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? – Ben Packard Oct 15 '12 at 21:43
  • 1
    It's only very new on the `dismissViewController`. Maybe it'll come to `popViewController`. File a radar :-). – mattjgalloway Oct 15 '12 at 21:47
  • Seriously though, do file a radar. It's more likely to make it in if people ask for it. – mattjgalloway Oct 15 '12 at 21:54
  • I just tried on bugreport.apple.com - are feature requests some place else? – Ben Packard Oct 15 '12 at 21:56
  • 1
    That's the right place to ask for it. There's an option for the classification to be 'Feature'. – mattjgalloway Oct 15 '12 at 21:58
  • 3
    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. – Jason Coco Oct 15 '12 at 22:07
  • Yep that's true, you could do it in that as well, good point. – mattjgalloway Oct 15 '12 at 22:09
  • Similar limitations though right (e.g. having to test if the animation should fire based on some logic that prevents it in other cases)? – Ben Packard Oct 16 '12 at 00:42
11

For 2018 ...

if you have this ...

    navigationController?.popViewController(animated: false)
    // I want this to happen next, help! ->
    nextStep()

and you want to add a completion ...

    CATransaction.begin()
    navigationController?.popViewController(animated: true)
    CATransaction.setCompletionBlock({ [weak self] in
       self?.nextStep() })
    CATransaction.commit()

it's that simple.

Handy tip...

It's the same deal for the handy popToViewController call.

A typical thing is you have an onboarding stack of a zillion screens. When finally done, you go all the way back to your "base" screen, and then finally fire up the app.

So in the "base" screen, to go "all the way back", popToViewController(self

func onboardingStackFinallyComplete() {
    
    CATransaction.begin()
    navigationController?.popToViewController(self, animated: false)
    CATransaction.setCompletionBlock({ [weak self] in
        guard let self = self else { return }
        .. actually launch the main part of the app
    })
    CATransaction.commit()
}
Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719
7

Cleaned up Swift 4 version based on this answer.

extension UINavigationController {
    func pushViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping () -> Void) {
        self.pushViewController(viewController, animated: animated)
        self.callCompletion(animated: animated, completion: completion)
    }

    func popViewController(animated: Bool, completion: @escaping () -> Void) -> UIViewController? {
        let viewController = self.popViewController(animated: animated)
        self.callCompletion(animated: animated, completion: completion)
        return viewController
    }

    private func callCompletion(animated: Bool, completion: @escaping () -> Void) {
        if animated, let coordinator = self.transitionCoordinator {
            coordinator.animate(alongsideTransition: nil) { _ in
                completion()
            }
        } else {
            completion()
        }
    }
}
d4Rk
  • 6,622
  • 5
  • 46
  • 60
5

The completion block is called after the viewDidDisappear method is called on the presented view controller, So putting code in the viewDidDisappear method of the popped view controller should work the same as a completion block.

rdelmar
  • 103,982
  • 12
  • 207
  • 218
5

Swift 3 answer, thanks to this answer: https://stackoverflow.com/a/28232570/3412567

    //MARK:UINavigationController Extension
extension UINavigationController {
    //Same function as "popViewController", but allow us to know when this function ends
    func popViewControllerWithHandler(completion: @escaping ()->()) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        self.popViewController(animated: true)
        CATransaction.commit()
    }
    func pushViewController(viewController: UIViewController, completion: @escaping ()->()) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        self.pushViewController(viewController, animated: true)
        CATransaction.commit()
    }
}
Community
  • 1
  • 1
Benobab
  • 428
  • 1
  • 6
  • 25
5

Please refer to recent version(5.1) of Swifty & SDK-like way,

extension UINavigationController {
    func popViewController(animated: Bool, completion: (() -> ())? = nil) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        popViewController(animated: animated)
        CATransaction.commit()
    }
    func pushViewController(_ viewController: UIViewController, animated: Bool, completion: (() -> ())? = nil) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        pushViewController(viewController, animated: animated)
        CATransaction.commit()
    }
}
DragonCherry
  • 742
  • 7
  • 10
4

Swift 4 version with optional viewController parameter to pop to a specific one.

extension UINavigationController {
    func pushViewController(viewController: UIViewController, animated: 
        Bool, completion: @escaping () -> ()) {

        pushViewController(viewController, animated: animated)

        if let coordinator = transitionCoordinator, animated {
            coordinator.animate(alongsideTransition: nil) { _ in
                completion()
            }
        } else {
            completion()
        }
}

func popViewController(viewController: UIViewController? = nil, 
    animated: Bool, completion: @escaping () -> ()) {
        if let viewController = viewController {
            popToViewController(viewController, animated: animated)
        } else {
            popViewController(animated: animated)
        }

        if let coordinator = transitionCoordinator, animated {
            coordinator.animate(alongsideTransition: nil) { _ in
                completion()
            }
        } else {
            completion()
        }
    }
}
TejAces
  • 551
  • 5
  • 11
  • 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. – Sean Jan 30 '19 at 18:39
3

2020 Swift 5.1 way

This solution guarantee that completion is executed after popViewController is fully finished. You can test it by doing another operation on the NavigationController in completion: In all other solutions above the UINavigationController is still busy with popViewController operation and does not respond.

public class NavigationController: UINavigationController, UINavigationControllerDelegate
{
    private var completion: (() -> Void)?

    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
        delegate = self
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public override func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool)
    {
        if self.completion != nil {
            DispatchQueue.main.async(execute: {
                self.completion?()
                self.completion = nil
            })
        }
    }

    func popViewController(animated: Bool, completion: @escaping () -> Void) -> UIViewController?
    {
        self.completion = completion
        return super.popViewController(animated: animated)
    }
}
2

Just for completeness, I've made an Objective-C category ready to use:

// UINavigationController+CompletionBlock.h

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionBlock)

- (UIViewController *)popViewControllerAnimated:(BOOL)animated completion:(void (^)()) completion;

@end
// UINavigationController+CompletionBlock.m

#import "UINavigationController+CompletionBlock.h"

@implementation UINavigationController (CompletionBlock)

- (UIViewController *)popViewControllerAnimated:(BOOL)animated completion:(void (^)()) completion {
    [CATransaction begin];
    [CATransaction setCompletionBlock:^{
        completion();
    }];

    UIViewController *vc = [self popViewControllerAnimated:animated];

    [CATransaction commit];

    return vc;
}

@end
Diego Freniche
  • 5,225
  • 3
  • 32
  • 45
2

There is a pod called UINavigationControllerWithCompletionBlock which adds support for a completion block when both pushing and popping on a UINavigationController.

duncanc4
  • 1,191
  • 1
  • 9
  • 17
2

Use the next extension on your code: (Swift 4)

import UIKit

extension UINavigationController {

    func popViewController(animated: Bool = true, completion: @escaping () -> Void) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        popViewController(animated: animated)
        CATransaction.commit()
    }

    func pushViewController(_ viewController: UIViewController, animated: Bool = true, completion: @escaping () -> Void) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        pushViewController(viewController, animated: animated)
        CATransaction.commit()
    }
}
1

I achieved exactly this with precision using a block. I wanted my fetched results controller to show the row that was added by the modal view, only once it had fully left the screen, so the user could see the change happening. In prepare for segue which is responsible for showing the modal view controller, I set the block I want to execute when the modal disappears. And in the modal view controller I override viewDidDissapear and then call the block. I simply begin updates when the modal is going to appear and end updates when it disappears, but that is because I'm using a NSFetchedResultsController however you can do whatever you like inside the block.

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    if([segue.identifier isEqualToString:@"addPassword"]){

        UINavigationController* nav = (UINavigationController*)segue.destinationViewController;
        AddPasswordViewController* v = (AddPasswordViewController*)nav.topViewController;

...

        // makes row appear after modal is away.
        [self.tableView beginUpdates];
        [v setViewDidDissapear:^(BOOL animated) {
            [self.tableView endUpdates];
        }];
    }
}

@interface AddPasswordViewController : UITableViewController<UITextFieldDelegate>

...

@property (nonatomic, copy, nullable) void (^viewDidDissapear)(BOOL animated);

@end

@implementation AddPasswordViewController{

...

-(void)viewDidDisappear:(BOOL)animated{
    [super viewDidDisappear:animated];
    if(self.viewDidDissapear){
        self.viewDidDissapear(animated);
    }
}

@end
malhal
  • 26,330
  • 7
  • 115
  • 133
0

I found that the implementation of func navigationController(UINavigationController, didShow: UIViewController, animated: Bool) is the only working solution here. We may make it better using RxSwift:

import UIKit
import RxSwift
import RxCocoa

extension Reactive where Base: UINavigationController {
    func popToViewController(_ viewController: UIViewController, animated: Bool) -> ControlEvent<ShowEvent> {
        let source = didShow
            .filter { [weak viewController] event in
                viewController == event.0
            }
            .take(1)
        
        _ = base.popToViewController(viewController, animated: animated)
        
        return ControlEvent(events: source)
    }
}

Usage:


// let navigationController = UINavigationController(rootViewController: page1)
// navigationController.pushViewController(page2, animated: false)

navigationController.rx
    .popToViewController(page1, animated: true)
    .bind { _ in
        // pop completion
    }
    .disposed(by: disposeBag)
Roman Aliyev
  • 224
  • 2
  • 7
-1

I think viewDidDisappear(_ animated: Bool) function can help for this. It will be called when the view did disappeared completely.

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    //do the stuff here
}
Naresh
  • 869
  • 8
  • 17