Completion handler for UINavigationController "pushViewController:animated"?

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: ?

60709 次浏览

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

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");
}];

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

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

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

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

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

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");
}];

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.