UINavigationController的“pushViewController:animated”的完成处理程序是什么?

128

我正在创建一个使用 UINavigationController 来呈现下一个视图控制器的应用程序。 在 iOS5 中,有一种新方法来呈现 UIViewControllers

presentViewController:animated:completion:

现在我想知道为什么UINavigationController没有完成处理程序? 只有

pushViewController:animated:

我能否像新的 presentViewController:animated:completion: 一样创建自己的完成处理程序?


2
不完全等同于完成处理程序,但 viewDidAppear:animated: 允许您每次视图控制器出现在屏幕上时执行代码(viewDidLoad 仅在第一次加载视图控制器时执行)。 - Moxy
@Moxy,你是指 -(void)viewDidAppear:(BOOL)animated 吗? - George
3
对于2018年...实际上就是这个:https://dev59.com/KWEi5IYBdhLWcg3wCYKY#43017103 - Fattie
9个回答

156

参见 par的答案,这是另一种更为实用且更新的解决方案

UINavigationController 的动画是通过 CoreAnimation 实现的,因此将代码封装在 CATransaction 中并设置完成回调函数是有意义的。

Swift:

对于 Swift,我建议创建一个扩展(extension),如下所示

extension UINavigationController {

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

}

使用方法:

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

Objective-C

头文件:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

实现:

#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

1
我认为(尚未测试)如果呈现的视图控制器在其viewDidLoad或viewWillAppear实现中触发动画,那么这可能会提供不准确的结果。 我认为这些动画将在pushViewController:animated:返回之前启动 - 因此,只有新触发的动画完成后,完成处理程序才会被调用。 - Matt H.
1
@MattH. 今晚我做了几个测试,看起来当使用 pushViewController:animated:popViewController:animated 时,viewDidLoadviewDidAppear 的调用会在后续的运行循环周期中发生。因此,我的印象是,即使这些方法确实调用了动画,它们也不会成为代码示例中提供的事务的一部分。这是你担心的吗?因为这个解决方案非常简单而出色。 - LeffelMania
1
回顾这个问题,我认为总的来说@MattH.和@LeffelMania提到的担忧确实突出了该解决方案存在的一个有效问题 - 它最终假定事务在推送完成后完成,但框架不能保证这种行为。尽管如此,在didShowViewController中可以保证所涉及的视图控制器已显示。虽然这个解决方案非常简单,但我会质疑它的“面向未来性”。特别是考虑到随着iOS7/8引入的视图生命周期回调的变化。 - Sam
8
在 iOS 9 设备上似乎无法可靠地工作,请参阅下面我或 @par 给出的答案以获取替代方案。 - Mike Sprague
1
@ZevEisenberg 当然。我的回答是这个世界上的恐龙代码~~已经两年了。 - chrs
显示剩余7条评论

134

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

编辑:我已经添加了我的原始答案的Swift 3版本。在这个版本中,我删除了Swift 2版本中显示的示例共同动画,因为它似乎使很多人感到困惑。

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

1
你告诉视图控制器更新状态栏有特别的原因吗?传递nil作为动画块似乎可以正常工作。 - Mike Sprague
2
这是一个并行动画的示例(上面的注释表明它是可选的)。传递nil也是完全有效的。 - par
1
@par,你应该更有防御性,当 transitionCoordinator 为 nil 时调用 completion 吗? - Aurelien Porte
1
@cbowns 我不是100%确定这一点,因为我没有看到这种情况发生过,但如果您没有看到transitionCoordinator,那么很可能是在导航控制器的生命周期中过早地调用了此函数。请至少等到viewWillAppear()被调用后再尝试使用动画推送视图控制器。 - par
1
有些情况下对我不起作用。animated的值为true,但transitionCoordinator的值为nil。不知道为什么会发生这种情况。 - Emtiyaj Ali
显示剩余3条评论

38

基于par的答案(在iOS9下是唯一有效的),但更简单,并且去掉了else语句(否则可能导致不会调用完成):

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

对我来说不起作用。transitionCoordinator 对我来说是 nil。 - tcurdt
可以用。而且这个比被接受的更好,因为动画完成并不总是与推送完成相同。 - Anton Plebanovich
您缺少一个非动画情况下的DispatchQueue.main.async。这个方法的约定是完成处理程序是异步调用的,您不应该违反它,因为这可能会导致难以察觉的错误。 - Werner Altewischer

25

目前UINavigationController不支持此功能。但是可以使用UINavigationControllerDelegate

一种简单的方法是通过对UINavigationController进行子类化并添加完成块属性来实现:

@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

在推送新的视图控制器之前,您需要设置完成块:

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

这个新的子类可以在Interface Builder中分配,也可以像这样通过编程来使用:

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

8
添加一个映射到视图控制器的完成块列表可能会使其最有用,而一个新的方法,可能称为 pushViewController:animated:completion:,将使其成为一种优雅的解决方案。 - Hyperbole
1
2018年的注意事项只有这些... https://dev59.com/KWEi5IYBdhLWcg3wCYKY#43017103 - Fattie

10

这里是带有 Pop 的 Swift 4 版本。

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

万一有其他人需要这个,我分享一下。


4
如果你对此进行简单的测试,你会发现完成块在动画完成之前就被触发了。因此,这可能并没有提供许多人所期望的功能。 - horseshoe7

9

扩展@Klaas的答案(并由于这个问题),我直接向推送方法添加了完成块:

@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

以下是使用方法:

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

太棒了。非常感谢。 - Petar
如果... (self.pushedVC == viewController) { 是不正确的。您需要使用 isEqual: 来测试对象之间的相等性,即 [self.pushedVC isEqual:viewController] - Evan R
@EvanR 那可能更加技术上正确。你有没有看到在另一种比较实例的方式中出现错误? - Sam
@Sam,虽然我没有在这个例子中实现它,但是在测试与其他对象的相等性方面肯定会用到——请参阅苹果公司的文档:https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/ObjectComparison.html。你的比较方法在这种情况下总是有效吗? - Evan R
请注意,这些答案已经非常过时了。现在只是一行代码。https://dev59.com/KWEi5IYBdhLWcg3wCYKY#43017103 - Fattie
显示剩余2条评论

6
自iOS 7.0以来,您可以使用UIViewControllerTransitionCoordinator添加推送完成块:
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
这与UINavigationController的push、pop等操作不完全相同。 - Jon Willis

3

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

2

需要添加一些管道以实现此行为并保留设置外部代理的功能。

下面是一个记录在案的实现,可以保持代理功能:

LBXCompletingNavigationController


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接