Completion block for popViewController

IosCocoa TouchUiviewUiviewcontrollerUiviewanimation

Ios Problem Overview


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?

Ios Solutions


Solution 1 - Ios

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 - Ios

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 - Ios

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 - Ios

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 - Ios

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;
}

Solution 6 - Ios

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()
}

Solution 7 - Ios

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.

Solution 8 - Ios

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)
  }
}

Solution 9 - Ios

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()
}

Solution 10 - Ios

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()
        }
    }
}

Solution 11 - Ios

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.

Solution 12 - Ios

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()
    }
}

Solution 13 - Ios

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()
        }
    }
}

Solution 14 - Ios

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)
    }
}

Solution 15 - Ios

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()
    }
}

Solution 16 - Ios

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

Solution 17 - Ios

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()
    }
}

Solution 18 - Ios

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

Solution 19 - Ios

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

Solution 20 - Ios

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
}

Solution 21 - Ios

For 2021, Swift 5

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(animated: Bool, completion: @escaping() -> ()) {
    popViewController(animated: animated)
    
    if let coordinator = transitionCoordinator, animated {
        coordinator.animate(alongsideTransition: nil) { _ in
            completion()
        }
    } else {
        completion()
    }
}
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBen PackardView Question on Stackoverflow
Solution 1 - IosJoris KluiversView Answer on Stackoverflow
Solution 2 - IosHotJardView Answer on Stackoverflow
Solution 3 - IosArbiturView Answer on Stackoverflow
Solution 4 - IosMuhammad WaqasView Answer on Stackoverflow
Solution 5 - IosJos JongView Answer on Stackoverflow
Solution 6 - IosVitaliiView Answer on Stackoverflow
Solution 7 - IosmattjgallowayView Answer on Stackoverflow
Solution 8 - IosrshevView Answer on Stackoverflow
Solution 9 - IosFattieView Answer on Stackoverflow
Solution 10 - Iosd4RkView Answer on Stackoverflow
Solution 11 - IosrdelmarView Answer on Stackoverflow
Solution 12 - IosBenobabView Answer on Stackoverflow
Solution 13 - IosTejAcesView Answer on Stackoverflow
Solution 14 - IosIliyan KafedzhievView Answer on Stackoverflow
Solution 15 - IosDragonCherryView Answer on Stackoverflow
Solution 16 - Iosduncanc4View Answer on Stackoverflow
Solution 17 - IosRigoberto Sáenz ImbacuánView Answer on Stackoverflow
Solution 18 - IosDiego FrenicheView Answer on Stackoverflow
Solution 19 - IosmalhalView Answer on Stackoverflow
Solution 20 - IosNareshView Answer on Stackoverflow
Solution 21 - IosNirajView Answer on Stackoverflow