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


110

UINavigationController打算使用创建一个应用程序来展示下一个视图控制器。使用iOS5,有一种新的呈现方法UIViewControllers

presentViewController:animated:completion:

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

pushViewController:animated:

是否可以像新创建自己的完成处理程序presentViewController:animated:completion:


2
与完成处理程序不完全相同,但是viewDidAppear:animated:让我们让您每次视图控制器出现在屏幕上时都执行代码(viewDidLoad仅是第一次加载视图控制器时)
Moxy 2012年

@Moxy,您是说-(void)viewDidAppear:(BOOL)animated
乔治

2
对于2018年 ...实际上就是这样:stackoverflow.com/a/43017103/294884
Fattie

Answers:


139

有关其他最新解决方案,请参阅标准答案

UINavigationController动画使用来运行CoreAnimation,因此将代码封装在其中CATransaction并设置完成块是很有意义的。

斯威夫特

为了快速起见,我建议这样创建一个扩展

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
}

目标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:animatedviewDidLoadand viewDidAppear调用发生在随后的运行循环周期中。因此,我的印象是,即使这些方法确实调用了动画,也不会成为代码示例中提供的事务的一部分。那是你的事吗?因为此解决方案非常简单。
LeffelMania 2015年

1
回顾这个问题,我一般认为@MattH提到的问题。和@LeffelMania确实突出了此解决方案的一个有效问题-它最终假定事务将在推送完成后完成,但是框架不能保证此行为。虽然可以保证比所讨论的视图控制器更好didShowViewController。尽管此解决方案非常简单,但我会质疑其“面向未来”。特别是考虑到ios7 / 8附带的查看生命周期回调的更改,
Sam

8
这似乎在iOS 9设备上无法可靠运行。看到我或@帕的回答下面一个替代
迈克·斯普拉格

1
@ZevEisenberg绝对。我的答案是
〜2

95

iOS 7以上版本的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版本中显示的示例共同动画,因为它似乎使很多人感到困惑。

斯威夫特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() }
    }
}

斯威夫特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
为什么要告诉vc更新其状态栏是否有特定原因?nil作为动画块传入似乎效果很好。
Mike Sprague 2015年

2
这是您可以做为并行动画的示例(其上方的注释表明它是可选的)。传递nil也是一件完全有效的事情。
标准杆

1
@par,您是否应该更具防御性,并且在n transitionCoordinator为零时调用补全?
Aurelien Porte

@AurelienPorte这是一个很好的收获,我会说是的,你应该。我将更新答案。

1
@cbowns我对此不太确定,因为我还没有看到这种情况,但是如果您没有看到a,transitionCoordinator那么您可能在导航控制器的生命周期中调用此函数的时间过早。至少等到viewWillAppear()被调用,然后再尝试使用动画推送视图控制器。

28

基于par的答案(这是唯一与iOS9兼容的答案),但更简单且缺少其他答案(这可能导致从未调用完成方法):

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对我来说是零。
tcurdt

为我工作。同样,这要比接受的更好,因为动画完成并不总是与推送完成相同。
安东·普勒巴诺维奇

对于非动画案例,您缺少DispatchQueue.main.async。这种方法的约定是,完成处理程序是异步调用的,您不应违反此方法,因为它可能导致细微的错误。
Werner Altewischer

24

目前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
NB为2018年真的就是这个... stackoverflow.com/a/43017103/294884
Fattie

8

这是带有流行音乐的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()
    }
}

以防万一其他人需要这个。


如果对此进行简单测试,则会发现完成块在动画完成之前触发。因此,这可能无法满足许多人的需求。
horseshoe7

7

为了扩展@Klaas的答案(并作为问题的结果),我将完成块直接添加到push方法中:

@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 2015年

if... (self.pushedVC == viewController) {是不正确的。您需要使用来测试对象之间的相等性isEqual:,即[self.pushedVC isEqual:viewController]
Evan R

@EvanR在技术上可能是正确的。您是否在以其他方式比较实例时看到错误?
山姆

@Sam不是专门针对此示例(未实现),但肯定是在测试与其他对象的相等性-请参阅Apple的文档:developer.apple.com/library/ios/documentation/General/…。在这种情况下,您的比较方法是否始终有效?
Evan R

我没有看到它不起作用,否则我会更改答案。据我所知,iOS并没有像Android那样通过活动来聪明地重新创建视图控制器。但是,isEqual如果他们这样做的话,从技术上讲可能会更正确。
山姆

5

从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等
乔恩·威利斯

3

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.