popViewController的完成块


113

使用来关闭模态视图控制器时dismissViewController,可以选择提供完成块。是否有类似的等效项popViewController

完成参数很方便。例如,我可以使用它来阻止从表视图中删除行,直到模式退出屏幕,让用户看到行动画。从推送视图控制器返回时,我希望有同样的机会。

我尝试放置popViewControllerUIView动画块中,但我确实可以访问完成块。但是,这会对弹出的视图产生一些不必要的副作用。

如果没有这样的方法,有什么解决方法?


stackoverflow.com/a/33767837/2774520我认为这种方式是最原生的一种
Oleksii Nezhyborets


3
对于2018年来说,这是非常简单和标准的: stackoverflow.com/a/43017103/294884
Fattie

Answers:


199

我知道两年前就已经接受了答案,但是这个答案是不完整的。

开箱即用地做您想做的事

从技术上讲这是正确的,因为UINavigationControllerAPI对此不提供任何选项。但是,通过使用CoreAnimation框架,可以向基础动画添加完成块:

[CATransaction begin];
[CATransaction setCompletionBlock:^{
    // handle completion here
}];

[self.navigationController popViewControllerAnimated:YES];

[CATransaction commit];

结束使用的动画会立即调用完成块popViewControllerAnimated:。此功能自iOS 4起可用。


5
我将其放在Swift中UINavigationController的扩展中:extension UINavigationController { func popViewControllerWithHandler(handler: ()->()) { CATransaction.begin() CATransaction.setCompletionBlock(handler) self.popViewControllerAnimated(true) CATransaction.commit() } }
Arbitur'1

1
当我在dismissViewController上执行completionHandler时,似乎不适合我,显示该视图的视图是视图层次结构的一部分。当我对CATransaction进行同样的操作时,我得到一个警告,该视图不属于视图层次结构。
moger777

1
好的,如果您反转开始和完成块,则看起来像您的作品。抱歉,投下赞成票,但堆栈溢出不会让我改变:(
moger777

7
是的,这看起来很棒,但似乎不起作用(至少在iOS 8上如此)。完成块立即被调用。可能是由于核心动画与UIView样式动画的混合。
stickj 2015年

5
不起作用

51

对于iOS9 SWIFT版本-就像一个超级按钮(尚未针对较早版本进行测试)。根据这个答案

extension UINavigationController {    
    func pushViewController(viewController: UIViewController, animated: Bool, completion: () -> ()) {
        pushViewController(viewController, animated: animated)

        if let coordinator = transitionCoordinator() where animated {
            coordinator.animateAlongsideTransition(nil) { _ in
                completion()
            }
        } else {
            completion()
        }
    }

    func popViewController(animated: Bool, completion: () -> ()) {
        popViewControllerAnimated(animated)

        if let coordinator = transitionCoordinator() where animated {
            coordinator.animateAlongsideTransition(nil) { _ in
                completion()
            }
        } else {
            completion()
        }
    }
}

如果不设置动画,将无法正常工作,应在下一个运行循环中完成此操作,以使其正常运行。
rshev

@rshev为什么在下一个runloop上?
本·辛克莱

@Andy从我记得尝试过的东西来看,当时还没有传播任何东西。尝试尝试一下,喜欢听听它如何为您工作。
rshev

@rshev我想我以前也是一样,我必须仔细检查。当前测试运行良好。
本·辛克莱

1
@LanceSamaria我建议使用viewDidDisappear。检查导航栏是否可用,如果不可用–导航栏中没有显示导航栏,因此将其弹出。如果(self.navigationController == nil){触发您的操作}
HotJard

32

Swift@JorisKluivers答案制作了带有扩展名的版本。

push和制作完动画后,这将称为完成关闭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()
    }
}

对我来说,在iOS 8.4中,使用ObjC编写的代码块会向动画的一半方向触发。如果使用Swift(8.4)编写,这是否在适当的时候触发?
朱利安·F·韦纳特

@Arbitur完成块确实在调用popViewController或之后调用pushViewController,但是如果之后检查topViewController是什么,您会发现它仍然是旧的,就像poppush从未发生过……
Bogdan Razvan,

@BogdanRazvan之后呢?动画完成后,是否会调用完成闭包?
Arbitur

17

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

17

我遇到过同样的问题。而且由于必须多次在完成块链中使用它,因此我在UINavigationController子类中创建了此通用解决方案:

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

假设

@interface NavigationController : UINavigationController <UINavigationControllerDelegate>

@implementation NavigationController {
    void (^_completion)();
}

- (id) initWithRootViewController:(UIViewController *) rootViewController {
    self = [super initWithRootViewController:rootViewController];
    if (self) {
        self.delegate = self;
    }
    return self;
}

1
我真的很喜欢这种解决方案,我将尝试使用类别和关联的对象。
spstanley

@spstanley,您需要发布此pod :)
k06a 2014年


15

开箱即用,无法做您想做的事情。即没有带有完成块的方法可以从导航堆栈中弹出视图控制器。

我要做的是把逻辑放进去viewDidAppear。当视图完成显示在屏幕上时,将调用该方法。会在视图控制器出现的所有不同情况下调用它,但这应该没问题。

或者,您可以使用该UINavigationControllerDelegate方法navigationController:didShowViewController:animated:执行类似的操作。当导航控制器完成推入或弹出视图控制器时,将调用此方法。


我尝试过了。我正在存储“已删除行索引”的数组,每当视图出现时,都会检查是否需要删除任何内容。它很快变得笨拙,但我可能会再试试。我想知道苹果为什么要为一个过渡提供它,而不为另一个过渡提供它?
本·帕卡德

1
它在上只是非常新的dismissViewController。也许到了popViewController。归档雷达:-)。
mattjgalloway 2012年

不过,请认真提出申请。如果人们要求,则更有可能成功。
mattjgalloway 2012年

1
那是要求它的正确地方。可以将分类设为“功能”。
mattjgalloway 2012年

3
这个答案并不完全正确。虽然您不能像on那样设置新样式块-dismissViewController:animated:completionBlock:,但是可以通过导航控制器的委托来获取动画。动画完成后,-navigationController:didShowViewController:animated:将在委托上调用该动画,您可以在那里立即执行所需的任何操作。
杰森·可可

13

正确处理或不处理动画,还包括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)
  }
}

调用completion()异步的任何特殊原因?
leviathan

1
使用协调completion器进行动画时,永远不要在同一运行循环上执行。这样可以保证completion在没有动画时永远不会在同一个runloop上运行。最好不要出现这种不一致的情况。
rshev

11

基于@HotJard的答案,当您只需要几行代码时。快捷方便。

斯威夫特4

_ = self.navigationController?.popViewController(animated: true)
self.navigationController?.transitionCoordinator.animate(alongsideTransition: nil) { _ in
    doWhatIWantAfterContollerHasPopped()
}

6

对于2018年...

如果你有这个...

    navigationController?.popViewController(animated: false)
    // I want this to happen next, help! ->
    nextStep()

而你想添加一个完成...

    CATransaction.begin()
    navigationController?.popViewController(animated: true)
    CATransaction.setCompletionBlock({ [weak self] in
       self?.nextStep() })
    CATransaction.commit()

就这么简单。

方便的提示...

方便的popToViewController通话也一样。

一个典型的情况是,您有一个成千上万个屏幕的入门堆栈。最终完成后,您将一直回到“基本”屏幕,然后最终启动该应用程序。

因此,在“基本”屏幕中,“一直返回”, 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()
}

5

在提供的视图控制器上调用viewDidDisappear方法之后,将调用完成块,因此将代码放入弹出的视图控制器的viewDidDisappear方法中应与完成块相同。


当然-除非您必须处理所有其他原因导致视图消失的情况。
本·帕卡德

1
@BenPackard,是的,将其放入您接受的答案中的viewDidAppear中也是如此。
rdelmar 2012年

5

Swift 3的答案,感谢这个答案:https : //stackoverflow.com/a/28232570/3412567

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

4

Swift 4版本,带有可选的viewController参数,可以弹出特定的参数。

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

可以接受的答案似乎可以在我的开发环境中与所有模拟器/设备一起使用,但是仍然会从生产用户那里报告错误。不知道这是否可以解决生产问题,但让我投票吧,如果有人从接受的答案中得到相同的问题,可以尝试一下。
肖恩

4

根据此答案清理了Swift 4版本。

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


2

2020 Swift 5.1方式

此解决方案保证在popViewController完全完成之后执行完成。您可以通过完成对NavigationController的其他操作来对其进行测试:在上述所有其他解决方案中,UINavigationController仍在忙于popViewController操作,并且没有响应。

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

1

为了完整起见,我已经准备好要使用的Objective-C类别:

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

1

我使用一个块精确地实现了这一目标。我希望获取的结果控制器仅在完全离开屏幕后才显示模式视图添加的行,以便用户可以看到发生的更改。在准备负责显示模态视图控制器的segue时,我设置了模态消失时要执行的块。在模态视图控制器中,我重写viewDidDissapear,然后调用该块。我只是在模态即将出现时开始更新,而在模态消失时结束更新,但这是因为我使用的是NSFetchedResultsController,但是您可以在块内执行任何操作。

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

1

在代码上使用下一个扩展:(快速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()
    }
}
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.