UINavigationControllerの完了ハンドラー「pushViewController:animated」?


110

UINavigationController次のビューコントローラーを表示するためにを使用してアプリを作成します。iOS5では、プレゼンテーションに新しい方法がありUIViewControllersます。

presentViewController:animated:completion:

なぜ完了ハンドラがUINavigationControllerないのですか?ただあります

pushViewController:animated:

newのような独自の完了ハンドラを作成することは可能presentViewController:animated:completion:ですか?


2
完了ハンドラーとまったく同じではありませんが、viewDidAppear:animated:ビューコントローラーが画面に表示されるたびにコードを実行してみましょう(viewDidLoadビューコントローラーが初めてロードされたときのみ)
Moxy

@モクシー、という意味ですか-(void)viewDidAppear:(BOOL)animated
ジョージ

2
以下のために2018 ...本当にそれだけで、このです:stackoverflow.com/a/43017103/294884
Fattie

回答:


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
}

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に、viewDidLoadとのviewDidAppear呼び出しが後続のランループサイクルで発生するようです。だから私の印象は、これらのメソッドがアニメーションを呼び出しても、コード例で提供されているトランザクションの一部にはならないということです。それはあなたの懸念でしたか?このソリューションは非常にシンプルだからです。
LeffelMania

1
この質問を振り返ってみると、@ MattHによって言及された懸念は一般的に考えられます。@LeffelManiaはこのソリューションの有効な問題を強調しています-プッシュが完了した後にトランザクションが完了することを最終的に想定していますが、フレームワークはこの動作を保証していません。問題のビューコントローラーが表示されているよりも保証されてdidShowViewControllerいます。この解決策は素晴らしく単純ですが、私はその「将来性」に疑問を投げかけます。特に、ios7 / 8に付属するライフサイクルコールバックを表示するための変更を考慮
Sam

8
これは、iOS 9デバイスでは確実に機能しないようです。代替案については、以下の私の回答または@parの回答を参照してください
Mike Sprague

1
@ZevEisenberg間違いなく。私の答えは、この世界の恐竜のコードです~~ 2歳
16

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

2
これは、並列アニメーションとして実行できる例の1つです(そのすぐ上のコメントはオプションであることを示しています)。渡すことnilも、完全に有効なことです。
2015

1
@par、より防御的になり、transitionCoordinatornilのときに完了を呼び出す必要がありますか?
Aurelien Porte 2016

@AurelienPorteそれは素晴らしいキャッチであり、そうだと思います。答えを更新します。
パー

1
@cbownsこれが起こったのを見たことがないので、私はこれについて100%確信が持てませんが、それが表示されない場合transitionCoordinatorは、ナビゲーションコントローラのライフサイクルの早い段階でこの関数を呼び出している可能性があります。viewWillAppear()アニメーションでビューコントローラをプッシュする前に、少なくとも呼び出されるまで待機します。
パー

28

パーの答え(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は私にとってはnilです。
tcurdt

私のために働く。また、アニメーションの完了がプッシュ完了と常に同じとは限らないため、これは受け入れられたものよりも優れています。
アントンプレバノビッチ2018

アニメーション化されていない場合の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
View Controllerにマップされた完了ブロックのリストを追加すると、おそらくこれが最も便利になり、おそらく呼び出される新しいメソッドは、pushViewController:animated:completion:これをエレガントなソリューションにします。
Hyperbole 2013

1
2018年の注
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

if... (self.pushedVC == viewController) {間違っています。を使用してオブジェクト間の同等性をテストする必要がありますisEqual:。つまり、[self.pushedVC isEqual:viewController]
Evan R

@EvanRそれはおそらくより技術的に正しいです。インスタンスを別の方法で比較するときにエラーが発生しましたか?
Sam

@Samは特にこの例ではありません(実装していませんでした)が、他のオブジェクトとの同等性のテストでは間違いありません。これに関するAppleのドキュメントを参照してください:developer.apple.com/library/ios/documentation/General/…。この場合、比較方法は常に機能しますか?
Evan R

私はそれがうまくいかないのを見なかったか、私の答えを変えたでしょう。私が知る限り、iOSはアクティビティでandroidが行うようにビューコントローラーを再作成するための賢いことは何もしません。しかし、はい、isEqual彼らがこれまで行った場合には、おそらくより技術的に正しいでしょう。
Sam

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プッシュ、ポップ、と全く同じものではありません
ジョン・ウィリス

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.