Abschlusshandler für UINavigationController "pushViewController: animiert"?

110

Ich möchte eine App mit a erstellen UINavigationController, um die nächsten Ansichts-Controller zu präsentieren. Mit iOS5 gibt es eine neue Methode zur Präsentation UIViewControllers:

presentViewController:animated:completion:

Jetzt frage ich mich, warum es keinen Completion-Handler gibt UINavigationController. Es gibt nur

pushViewController:animated:

Ist es möglich, meinen eigenen Completion-Handler wie den neuen zu erstellen presentViewController:animated:completion:?

geforce
quelle
2
Nicht genau das Gleiche wie ein Completion-Handler, aber viewDidAppear:animated:Sie können jedes Mal Code ausführen, wenn Ihr View-Controller auf dem Bildschirm angezeigt wird ( viewDidLoadnur beim ersten Laden Ihres View-Controllers)
Moxy
@Moxy, meinst du-(void)viewDidAppear:(BOOL)animated
George
2
für 2018 ... wirklich ist es nur das: stackoverflow.com/a/43017103/294884
Fattie

Antworten:

139

In der Antwort von par finden Sie eine weitere und aktuellere Lösung

UINavigationControllerAnimationen werden mit ausgeführt CoreAnimation, daher wäre es sinnvoll, den Code darin zu kapseln CATransactionund so einen Abschlussblock festzulegen.

Swift :

Für schnell schlage ich vor, eine Erweiterung als solche zu erstellen

extension UINavigationController {

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

}

Verwendung:

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

Ziel c

Header:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

Implementierung:

#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
chrs
quelle
1
Ich glaube (habe nicht getestet), dass dies zu ungenauen Ergebnissen führen kann, wenn der vorgestellte View Controller Animationen in seinen viewDidLoad- oder viewWillAppear-Implementierungen auslöst. Ich denke, diese Animationen werden vor pushViewController gestartet: animiert: kehrt zurück - daher wird der Abschluss-Handler erst aufgerufen, wenn die neu ausgelösten Animationen beendet sind.
Matt H.
1
@ MattH. Habe heute Abend ein paar Tests durchgeführt und es sieht so aus, als ob bei Verwendung von pushViewController:animated:oder popViewController:animateddie Aufrufe viewDidLoadund viewDidAppearin nachfolgenden Runloop-Zyklen auftreten. Mein Eindruck ist also, dass diese Methoden, selbst wenn sie Animationen aufrufen, nicht Teil der im Codebeispiel angegebenen Transaktion sind. War das dein Anliegen? Weil diese Lösung fabelhaft einfach ist.
LeffelMania
1
Wenn ich auf diese Frage zurückblicke, denke ich im Allgemeinen an die von @MattH erwähnten Bedenken. und @LeffelMania heben ein gültiges Problem mit dieser Lösung hervor - es wird letztendlich davon ausgegangen, dass die Transaktion nach Abschluss des Push abgeschlossen wird, aber das Framework garantiert dieses Verhalten nicht. Es ist jedoch garantiert, dass der betreffende View Controller angezeigt wird didShowViewController. Während diese Lösung fantastisch einfach ist, würde ich ihre "Zukunftssicherheit" in Frage stellen. Besonders angesichts der Änderungen beim Anzeigen von Lebenszyklus-Rückrufen, die mit ios7 / 8
Sam
8
Dies scheint auf iOS 9-Geräten nicht zuverlässig zu funktionieren. Siehe meine oder @ par Antworten unten für eine Alternative
Mike Sprague
1
@ ZevEisenberg auf jeden Fall. Meine Antwort ist Dinosaurier-Code in dieser Welt ~~ 2 Jahre alt
chrs
95

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: Ich habe eine Swift 3-Version meiner ursprünglichen Antwort hinzugefügt. In dieser Version habe ich die in der Swift 2-Version gezeigte Beispiel-Co-Animation entfernt, da sie viele Leute verwirrt zu haben scheint.

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()
            }
        )
    }
}
Par
quelle
1
Gibt es einen bestimmten Grund, warum Sie den VC anweisen, seine Statusleiste zu aktualisieren? Dies scheint gut zu funktionieren, nilwenn Sie als Animationsblock übergeben werden.
Mike Sprague
2
Dies ist ein Beispiel für eine parallele Animation (der Kommentar direkt darüber zeigt an, dass dies optional ist). Übergeben nilist auch eine absolut gültige Sache.
Par
1
@par, solltest du defensiver sein und den Abschluss nennen, wenn der transitionCoordinatorNull ist?
Aurelien Porte
@ AurelienPorte Das ist ein toller Fang und ich würde ja sagen, du solltest. Ich werde die Antwort aktualisieren.
Par
1
@cbowns Ich bin mir nicht 100% sicher, da ich dies nicht gesehen habe. Wenn Sie jedoch keine sehen, transitionCoordinatorrufen Sie diese Funktion wahrscheinlich zu früh im Lebenszyklus des Navigationscontrollers auf. Warten Sie mindestens, bis der viewWillAppear()Aufruf erfolgt, bevor Sie versuchen, einen Ansichts-Controller mit Animation zu verschieben.
Par
28

Basierend auf der Antwort von par (die die einzige war, die mit iOS9 funktionierte), aber einfacher und mit einem fehlenden anderen (was dazu führen könnte, dass die Fertigstellung nie aufgerufen wurde):

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()
        }
    }
}
Daniel
quelle
Funktioniert bei mir nicht Der TransitionCoordinator ist für mich gleich Null.
tcurdt
Funktioniert bei mir. Auch dieser ist besser als der akzeptierte, da der Abschluss der Animation nicht immer mit dem Abschluss des Push identisch ist.
Anton Plebanovich
Sie vermissen eine DispatchQueue.main.async für den nicht animierten Fall. Der Vertrag dieser Methode besteht darin, dass der Completion-Handler asynchron aufgerufen wird. Sie sollten dies nicht verletzen, da dies zu subtilen Fehlern führen kann.
Werner Altewischer
24

Derzeit unterstützt das UINavigationControllerdas nicht. Aber es gibt das UINavigationControllerDelegate, was Sie verwenden können.

Eine einfache Möglichkeit, dies zu erreichen, besteht darin UINavigationController, eine Abschlussblock-Eigenschaft zu unterordnen und hinzuzufügen:

@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

Bevor Sie den neuen Ansichts-Controller drücken, müssen Sie den Abschlussblock festlegen:

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

Diese neue Unterklasse kann entweder im Interface Builder zugewiesen oder programmgesteuert wie folgt verwendet werden:

PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];
Klaas
quelle
8
Das Hinzufügen einer Liste von Abschlussblöcken, die den Ansichtscontrollern zugeordnet sind, würde dies wahrscheinlich am nützlichsten machen, und eine neue Methode, die möglicherweise aufgerufen wird, pushViewController:animated:completion:würde dies zu einer eleganten Lösung machen.
Hyperbole
1
NB für 2018 ist es wirklich nur das ... stackoverflow.com/a/43017103/294884
Fattie
8

Hier ist die Swift 4 Version mit dem 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()
    }
}

Nur für den Fall, dass jemand anderes dies braucht.

Francois Nadeau
quelle
Wenn Sie einen einfachen Test durchführen, werden Sie feststellen, dass der Abschlussblock ausgelöst wird, bevor die Animation beendet ist. Das bietet also wahrscheinlich nicht das, wonach viele suchen.
Hufeisen7
7

Um die Antwort von @Klaas (und als Ergebnis dieser Frage) zu erweitern, habe ich der Push-Methode Vervollständigungsblöcke direkt hinzugefügt:

@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

Wie folgt zu verwenden:

UIViewController *vc = ...;
[(PbNavigationController *)self.navigationController pushViewController:vc animated:YES completion:^ {
    NSLog(@"COMPLETED");
}];
Sam
quelle
Brillant. Vielen Dank
Petar
if... (self.pushedVC == viewController) {ist falsch. Sie müssen Test Gleichheit unter den Objekten durch die Verwendung isEqual:, das heißt[self.pushedVC isEqual:viewController]
Evan R
@EvanR das ist wohl technisch korrekter ja. Haben Sie einen Fehler beim Vergleich der Instanzen in die andere Richtung gesehen?
Sam
@Sam nicht speziell mit diesem Beispiel (hat es nicht implementiert), aber definitiv beim Testen der Gleichheit mit anderen Objekten - siehe Apples Dokumente dazu: developer.apple.com/library/ios/documentation/General/… . Funktioniert Ihre Vergleichsmethode in diesem Fall immer?
Evan R
Ich habe nicht gesehen, dass es nicht funktioniert, sonst hätte ich meine Antwort geändert. Soweit ich weiß, macht iOS nichts Kluges, um View Controller neu zu erstellen, wie es Android bei Aktivitäten tut. aber ja, isEqualwäre wahrscheinlich technisch korrekter, falls sie es jemals getan hätten.
Sam
5

Seit iOS 7.0 können Sie UIViewControllerTransitionCoordinatoreinen Push-Abschlussblock hinzufügen:

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");
}];
wj2061
quelle
1
Dies ist nicht ganz das Gleiche wie UINavigationController Push, Pop usw.
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
        }
    }
}
rahul_send89
quelle
2

Das Hinzufügen dieses Verhaltens und das Beibehalten eines externen Delegaten erfordert etwas mehr Arbeit.

Hier ist eine dokumentierte Implementierung, die die Delegatenfunktionalität beibehält:

LBXCompletingNavigationController

nzeltzer
quelle