Completion block for popViewController
Asked Answered
V

21

140

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?

Vervain answered 15/10, 2012 at 21:36 Comment(4)
https://mcmap.net/q/167924/-completion-handler-for-uinavigationcontroller-quot-pushviewcontroller-animated-quot i think this way is the most native oneRiddance
Possible duplicate of Is it possible to push a View Controller with a completion block?Munafo
For 2018 this is very simple and standard: https://mcmap.net/q/167925/-is-it-possible-to-push-a-view-controller-with-a-completion-blockMunafo
You can use this extension gist.github.com/GoNinja/74ab2cfd0d3c3539d63686e551f6c1b4Foote
S
216

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.

Sexology answered 1/12, 2014 at 13:56 Comment(15)
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() } }Internode
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.Wolcott
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 :(Wolcott
Does not work for me on iOS 7, the completion block is called immediately.Harty
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!Bibi
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.Grison
This works for me when pushing a view controller but not when popping on iOS 8. My completion block never fires.Molotov
Interesting. For me, in iOS 8.4, the completion block does fire, but approx. half the way down the animation.Become
THIS DOES NOT WORKHeartburning
when you add an alertview or some animation in viewdidload, it will not work immediatelyDre
Prematurely added +1. Should be -1. This does not work consistently, which is kind of worse than it not working at all.Nebraska
milions of thanks working for me please note use pushviewcontroller either sometimes it not working :)Llovera
This does not work if I am using navigationController?.popToRootViewController; it works if I am using navigationController?.popViewController (at least on iOS 13.1).Substandard
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.Pecos
sometimes it doesn't workPlain
P
81

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()
        }
    }
}
Paestum answered 23/4, 2016 at 10:51 Comment(7)
@rshev why on next runloop?Lenora
@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.Tishtisha
@Tishtisha I think I had it the same way before, I have to double check. Current tests run fine.Lenora
@Paestum 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?Vinegarish
@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 }Paestum
@Paestum you forgot to add @escaping on the completion handler.Punk
This is not Swift 5, if let where was removed in Swift 3! Also, transitionCoordinator() is wrong, it's a property not a method.Cesarean
I
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()
    }
}
Internode answered 30/1, 2015 at 9:14 Comment(5)
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)?Become
@Internode 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...Equiponderance
@BogdanRazvan right afterwards what? Does your completion closure get called once the animation is complete?Internode
@Internode 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.Equiponderance
sometimes it doesn't workPlain
V
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()
}
}
Vaud answered 14/9, 2018 at 15:45 Comment(0)
G
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;
}
Glomma answered 13/12, 2013 at 11:48 Comment(4)
I really like this solution, I'm going to try it with a category and an associated object.Narrative
@Narrative you need to publish this pod :)Unity
Swift version -> https://mcmap.net/q/165527/-completion-block-for-popviewcontrollerPecos
While this does work the issue is that it restricts other use of UINavigationControllerDelegateBearden
T
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)
  }
}
Tishtisha answered 6/9, 2016 at 14:51 Comment(2)
Any particular reason why you're calling the completion() async?Washhouse
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.Tishtisha
B
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()
}
Banquer answered 25/5, 2018 at 15:50 Comment(0)
B
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.

Bibi answered 15/10, 2012 at 21:39 Comment(8)
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?Vervain
It's only very new on the dismissViewController. Maybe it'll come to popViewController. File a radar :-).Bibi
Seriously though, do file a radar. It's more likely to make it in if people ask for it.Bibi
I just tried on bugreport.apple.com - are feature requests some place else?Vervain
That's the right place to ask for it. There's an option for the classification to be 'Feature'.Bibi
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.Deoxidize
Yep that's true, you could do it in that as well, good point.Bibi
Similar limitations though right (e.g. having to test if the animation should fire based on some logic that prevents it in other cases)?Vervain
M
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()
}
Munafo answered 18/5, 2018 at 17:7 Comment(0)
H
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()
        }
    }
}
Harmattan answered 19/6, 2018 at 9:30 Comment(0)
E
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.

Elinoreeliot answered 15/10, 2012 at 21:51 Comment(2)
Sure - except then you have to handle all the cases where the view is disappearing for some other reason.Vervain
@BenPackard, yes, and the same is true for putting it in viewDidAppear in the answer you accepted.Elinoreeliot
I
5

Swift 3 answer, thanks to this answer: https://mcmap.net/q/165527/-completion-block-for-popviewcontroller

    //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()
    }
}
Impassible answered 9/11, 2016 at 8:6 Comment(0)
F
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()
    }
}
Famulus answered 6/4, 2021 at 0:36 Comment(0)
A
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()
        }
    }
}
Amalia answered 17/6, 2018 at 20:8 Comment(1)
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.Spencerianism
P
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)
    }
}
Pecos answered 6/2, 2020 at 8:34 Comment(0)
K
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
Kronick answered 27/11, 2015 at 12:15 Comment(0)
A
2

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

Appendicle answered 15/2, 2016 at 23:33 Comment(0)
G
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()
    }
}
Gaylor answered 4/1, 2018 at 21:42 Comment(0)
M
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
Mojica answered 27/12, 2015 at 1:34 Comment(0)
E
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)
Eyra answered 31/7, 2022 at 1:17 Comment(0)
K
-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
}
Kean answered 28/5, 2021 at 10:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.