132

I'm about creating an app using a UINavigationController to present the next view controllers. With iOS5 there´s a new method to presenting UIViewControllers:

presentViewController:animated:completion:

Now I ask me why isn´t there a completion handler for UINavigationController? There are just

pushViewController:animated:

Is it possible to create my own completion handler like the new presentViewController:animated:completion: ?

3
  • 2
    not exactly the same thing as a completion handler but viewDidAppear:animated: let's you execute code each time your view controller appears on screen (viewDidLoad only the first time your view controller is loaded) Commented Aug 5, 2012 at 15:17
  • @Moxy, do you mean -(void)viewDidAppear:(BOOL)animated Commented Nov 27, 2013 at 12:54
  • 3
    for 2018 ... really it's just this: stackoverflow.com/a/43017103/294884 Commented Jan 4, 2018 at 22:29

10 Answers 10

156

See par's answer for another and more up to date solution

UINavigationController animations are run with CoreAnimation, so it would make sense to encapsulate the code within CATransaction and thus set a completion block.

Swift:

For swift I suggest creating an extension as such

extension UINavigationController {

  public func pushViewController(viewController: UIViewController,
                                 animated: Bool,
                                 completion: @escaping (() -> Void)?) {
    CATransaction.begin()
    CATransaction.setCompletionBlock(completion)
    pushViewController(viewController, animated: animated)
    CATransaction.commit()
  }

}

Usage:

navigationController?.pushViewController(vc, animated: true) {
  // Animation done
}

Objective-C

Header:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

Implementation:

#import "UINavigationController+CompletionHandler.h"
#import <QuartzCore/QuartzCore.h>

@implementation UINavigationController (CompletionHandler)

- (void)completionhandler_pushViewController:(UIViewController *)viewController 
                                    animated:(BOOL)animated 
                                  completion:(void (^)(void))completion 
{
    [CATransaction begin];
    [CATransaction setCompletionBlock:completion];
    [self pushViewController:viewController animated:animated];
    [CATransaction commit];
}

@end
Sign up to request clarification or add additional context in comments.

12 Comments

I believe (haven't tested) that this could provide inaccurate results if the presented view controller triggers animations inside it's viewDidLoad or viewWillAppear implementations. I think those animations will be started before pushViewController:animated: returns -- thus, the completion handler will not get called until the newly-triggered animations have finished.
@MattH. Did a couple tests this evening and it looks like when using pushViewController:animated: or popViewController:animated, the viewDidLoad and viewDidAppear calls happen in subsequent runloop cycles. So my impression is that even if those methods do invoke animations, they won't be part of transaction provided in the code example. Was that your concern? Because this solution is fabulously simple.
Looking back at this question, I think in general the concerns mentioned by @MattH. and @LeffelMania do highlight a valid problem with this solution - it ultimately assumes the transaction will be completed after the push is complete, but the framework does not guarantee this behaviour. It is guaranteed than the view controller in question is shown in didShowViewController though. While this solution is fantastically simple, I would question its "future-proof-ness". Especially given the changes to view lifecycle callbacks that came with ios7/8
This doesn't seem to work reliably on iOS 9 devices. See my or @par's answers below for an alternative
@ZevEisenberg definitely. My answer is dinosaur code in this world ~~2 years old
|
139

iOS 7+ Swift

Swift 4:

// 2018.10.30 par:
//   I've updated this answer with an asynchronous dispatch to the main queue
//   when we're called without animation. This really should have been in the
//   previous solutions I gave but I forgot to add it.
extension UINavigationController {
    public func pushViewController(
        _ viewController: UIViewController,
        animated: Bool,
        completion: @escaping () -> Void)
    {
        pushViewController(viewController, animated: animated)

        guard animated, let coordinator = transitionCoordinator else {
            DispatchQueue.main.async { completion() }
            return
        }

        coordinator.animate(alongsideTransition: nil) { _ in completion() }
    }

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

        guard animated, let coordinator = transitionCoordinator else {
            DispatchQueue.main.async { completion() }
            return
        }

        coordinator.animate(alongsideTransition: nil) { _ in completion() }
    }
}

EDIT: I've added a Swift 3 version of my original answer. In this version I've removed the example co-animation shown in the Swift 2 version as it seems to have confused a lot of people.

Swift 3:

import UIKit

// Swift 3 version, no co-animation (alongsideTransition parameter is nil)
extension UINavigationController {
    public func pushViewController(
        _ viewController: UIViewController,
        animated: Bool,
        completion: @escaping (Void) -> Void)
    {
        pushViewController(viewController, animated: animated)

        guard animated, let coordinator = transitionCoordinator else {
            completion()
            return
        }

        coordinator.animate(alongsideTransition: nil) { _ in completion() }
    }
}

Swift 2:

import UIKit

// Swift 2 Version, shows example co-animation (status bar update)
extension UINavigationController {
    public func pushViewController(
        viewController: UIViewController,
        animated: Bool,
        completion: Void -> Void)
    {
        pushViewController(viewController, animated: animated)

        guard animated, let coordinator = transitionCoordinator() else {
            completion()
            return
        }

        coordinator.animateAlongsideTransition(
            // pass nil here or do something animated if you'd like, e.g.:
            { context in
                viewController.setNeedsStatusBarAppearanceUpdate()
            },
            completion: { context in
                completion()
            }
        )
    }
}

9 Comments

Is there a particular reason why you're telling the vc to update it's status bar? This seems to work fine passing nil in as the animation block.
It's an example of something you might do as a parallel animation (the comment immediately above it indicates it's optional). Passing nil is a perfectly valid thing to do too.
@par, Should you be more defensive and call the completion when the transitionCoordinator is nil?
@cbowns I'm not 100% sure about this as I haven't seen this happen, but if you don't see a transitionCoordinator then it's likely you're calling this function too early in the navigation controller's lifecycle. Wait at least until viewWillAppear() is called before trying to push a view controller with animation.
Not working for me for some cases. The value of animated is true but the transitionCoordinator value is nil. Dont know why is this happening.
|
38

Based on par's answer (which was the only one that worked with iOS9), but simpler and with a missing else (which could have led to the completion never being called):

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

3 Comments

Does not work for me. The transitionCoordinator is nil for me.
Works for me. Also this one is better then accepted one because animation completion not always the same as push completion.
You're missing a DispatchQueue.main.async for the non animated case. The contract of this method is that the completion handler is called asynchronously, you should not violated this because it can lead to subtle bugs.
25

Currently the UINavigationController does not support this. But there's the UINavigationControllerDelegate that you can use.

An easy way to accomplish this is by subclassing UINavigationController and adding a completion block property:

@interface PbNavigationController : UINavigationController <UINavigationControllerDelegate>

@property (nonatomic,copy) dispatch_block_t completionBlock;

@end


@implementation PbNavigationController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        self.delegate = self;
    }
    return self;
}

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    NSLog(@"didShowViewController:%@", viewController);

    if (self.completionBlock) {
        self.completionBlock();
        self.completionBlock = nil;
    }
}

@end

Before pushing the new view controller you would have to set the completion block:

UIViewController *vc = ...;
((PbNavigationController *)self.navigationController).completionBlock = ^ {
    NSLog(@"COMPLETED");
};
[self.navigationController pushViewController:vc animated:YES];

This new subclass can either be assigned in Interface Builder or be used programmatically like this:

PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];

2 Comments

Adding a list of completion blocks mapped to view controllers would probably make this most useful, and a new method, perhaps called pushViewController:animated:completion: would make this an elegant solution.
NB for 2018 it's really just this ... stackoverflow.com/a/43017103/294884
10

Here is the Swift 4 version with the Pop.

extension UINavigationController {
    public func pushViewController(viewController: UIViewController,
                                   animated: Bool,
                                   completion: (() -> Void)?) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        pushViewController(viewController, animated: animated)
        CATransaction.commit()
    }

    public func popViewController(animated: Bool,
                                  completion: (() -> Void)?) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        popViewController(animated: animated)
        CATransaction.commit()
    }
}

Just in case someone else needs this.

1 Comment

If you run a simple test on this, you'll find that the completion block fires before the animation is finished. So this probably doesn't provide what many are looking for.
9

To expand on @Klaas' answer (and as a result of this question) I've added completion blocks directly to the push method:

@interface PbNavigationController : UINavigationController <UINavigationControllerDelegate>

@property (nonatomic,copy) dispatch_block_t completionBlock;
@property (nonatomic,strong) UIViewController * pushedVC;

@end


@implementation PbNavigationController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        self.delegate = self;
    }
    return self;
}

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    NSLog(@"didShowViewController:%@", viewController);

    if (self.completionBlock && self.pushedVC == viewController) {
        self.completionBlock();
    }
    self.completionBlock = nil;
    self.pushedVC = nil;
}

-(void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    if (self.pushedVC != viewController) {
        self.pushedVC = nil;
        self.completionBlock = nil;
    }
}

-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(dispatch_block_t)completion {
    self.pushedVC = viewController;
    self.completionBlock = completion;
    [self pushViewController:viewController animated:animated];
}

@end

To be used as follows:

UIViewController *vc = ...;
[(PbNavigationController *)self.navigationController pushViewController:vc animated:YES completion:^ {
    NSLog(@"COMPLETED");
}];

6 Comments

if... (self.pushedVC == viewController) { is incorrect. You need to test equality among objects by using isEqual:, i.e., [self.pushedVC isEqual:viewController]
@EvanR that is probably more technically correct yea. have you seen an error in comparing the instances the other way?
@Sam not specifically with this example (didn't implement it) but definitely in testing equality with other objects—see Apple's docs on this: developer.apple.com/library/ios/documentation/General/…. Does your method of comparison always work in this case?
I've not seen it not work or I would have changed my answer. As far as I know iOS doesn't do anything clever to recreate view controllers like android does with activities. but yes, isEqual would probably be more technically correct incase they ever did.
In this particular example you are interested in testing for identity rather than equality, so == is more correct. On top of that, unless isEqual: is explicitly overridden by the receiver, it would resort to identity comparison anyway.
|
6

Since iOS 7.0,you can use UIViewControllerTransitionCoordinator to add a push completion block:

UINavigationController *nav = self.navigationController;
[nav pushViewController:vc animated:YES];

id<UIViewControllerTransitionCoordinator> coordinator = vc.transitionCoordinator;
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {

} completion:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
    NSLog(@"push completed");
}];

1 Comment

This is not quite the same thing as UINavigationController push, pop, etc.
4

Swift 2.0

extension UINavigationController : UINavigationControllerDelegate {
    private struct AssociatedKeys {
        static var currentCompletioObjectHandle = "currentCompletioObjectHandle"
    }
    typealias Completion = @convention(block) (UIViewController)->()
    var completionBlock:Completion?{
        get{
            let chBlock = unsafeBitCast(objc_getAssociatedObject(self, &AssociatedKeys.currentCompletioObjectHandle), Completion.self)
            return chBlock as Completion
        }set{
            if let newValue = newValue {
                let newValueObj : AnyObject = unsafeBitCast(newValue, AnyObject.self)
                objc_setAssociatedObject(self, &AssociatedKeys.currentCompletioObjectHandle, newValueObj, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
    func popToViewController(animated: Bool,comp:Completion){
        if (self.delegate == nil){
            self.delegate = self
        }
        completionBlock = comp
        self.popViewControllerAnimated(true)
    }
    func pushViewController(viewController: UIViewController, comp:Completion) {
        if (self.delegate == nil){
            self.delegate = self
        }
        completionBlock = comp
        self.pushViewController(viewController, animated: true)
    }

    public func navigationController(navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool){
        if let comp = completionBlock{
            comp(viewController)
            completionBlock = nil
            self.delegate = nil
        }
    }
}

Comments

2

It takes a little more pipework to add this behavior and retain the ability to set an external delegate.

Here's a documented implementation that maintains delegate functionality:

LBXCompletingNavigationController

Comments

0

I've write an extension but in my case the call of methods are very haotic, so this way is not suitable for me. Anywhere, maybe somebody would need it.

private var UINavigationControllerCompletionKey = 0
private var UINavigationControllerDelegateKey = 1

extension UINavigationController {
    fileprivate var completion: EmptyClosure? {
        get {
            return objc_getAssociatedObject(self, &UINavigationControllerCompletionKey) as? EmptyClosure
        }
        set(newValue) {
            objc_setAssociatedObject(self, &UINavigationControllerCompletionKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    /// Need to save delegate in order to set it back after completion
    fileprivate var previousDelegateCache: UINavigationControllerDelegate? {
        get {
            return objc_getAssociatedObject(self, &UINavigationControllerDelegateKey) as? UINavigationControllerDelegate
        }
        set(newValue) {
            objc_setAssociatedObject(self, &UINavigationControllerDelegateKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    @discardableResult
    public func popViewController(animated: Bool, completion: @escaping EmptyClosure) -> UIViewController? {
        self.completion = completion
        previousDelegateCache = delegate
        delegate = self
        return self.popViewController(animated: animated)
    }

    @discardableResult
    public func popToRootViewController(animated: Bool, completion: @escaping EmptyClosure) -> [UIViewController]? {
        self.completion = completion
        previousDelegateCache = delegate
        delegate = self
        return self.popToRootViewController(animated: animated)
    }

    public func pushViewController(_ viewController: UIViewController, animated: Bool, completion: EmptyClosure?) {
        self.completion = completion
        previousDelegateCache = delegate
        delegate = self
        return self.pushViewController(viewController, animated: animated)
    }
}

extension UINavigationController: UINavigationControllerDelegate {
    public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        completion?()
        completion = nil
        delegate = previousDelegateCache
        previousDelegateCache = nil
    }
}  

Also check interesting repo with buffered NavigationController. https://github.com/Plasma/BufferedNavigationController/tree/master Although the problem with delegate is still exists in it.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.