Gestore di completamento per UINavigationController "pushViewController: animated"?


110

Sto creando un'app utilizzando a UINavigationControllerper presentare i controller di visualizzazione successivi. Con iOS5 c'è un nuovo metodo per presentare UIViewControllers:

presentViewController:animated:completion:

Ora mi chiedo perché non esiste un gestore di completamento UINavigationController? Ci sono solo

pushViewController:animated:

È possibile creare il mio gestore di completamento come il nuovo presentViewController:animated:completion:?


2
non esattamente la stessa cosa di un gestore di completamento, ma ti viewDidAppear:animated:consente di eseguire il codice ogni volta che il tuo controller di visualizzazione appare sullo schermo ( viewDidLoadsolo la prima volta che viene caricato il controller di visualizzazione)
Moxy

@Moxy, vuoi dire-(void)viewDidAppear:(BOOL)animated
George,

2
per 2018 ... in realtà è solo questo: stackoverflow.com/a/43017103/294884
Fattie

Risposte:


139

Vedi la risposta di par per un'altra e più aggiornata soluzione

UINavigationControllerle animazioni vengono eseguite con CoreAnimation, quindi avrebbe senso incapsulare il codice all'interno CATransactione quindi impostare un blocco di completamento.

Swift :

Per swift suggerisco di creare un'estensione in quanto tale

extension UINavigationController {

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

}

Uso:

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

Objective-C

Intestazione:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

Implementazione:

#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
Credo (non ho testato) che ciò potrebbe fornire risultati imprecisi se il controller di visualizzazione presentato attiva le animazioni all'interno delle sue implementazioni viewDidLoad o viewWillAppear. Penso che quelle animazioni verranno avviate prima che pushViewController: animated: ritorni - quindi, il gestore di completamento non verrà chiamato fino al termine delle animazioni appena attivate.
Matt H.

1
@MattH. Ho fatto un paio di test questa sera e sembra che quando si utilizza pushViewController:animated:o popViewController:animated, le chiamate viewDidLoade viewDidAppearavvengano nei cicli di runloop successivi. Quindi la mia impressione è che anche se quei metodi richiamano animazioni, non faranno parte della transazione fornita nell'esempio di codice. Questa era la tua preoccupazione? Perché questa soluzione è incredibilmente semplice.
LeffelMania

1
Guardando indietro a questa domanda, penso in generale alle preoccupazioni menzionate da @MattH. e @LeffelMania evidenziano un problema valido con questa soluzione: alla fine si presume che la transazione sarà completata dopo il completamento del push, ma il framework non garantisce questo comportamento. È garantito che il controller di visualizzazione in questione viene mostrato didShowViewController. Sebbene questa soluzione sia straordinariamente semplice, metterei in dubbio la sua "prova di futuro". Soprattutto date le modifiche alla visualizzazione dei callback del ciclo di vita fornite con ios7 / 8
Sam

8
Questo non sembra funzionare in modo affidabile sui dispositivi iOS 9. Vedi le risposte my or @ par di seguito per un'alternativa
Mike Sprague

1
@ZevEisenberg decisamente. La mia risposta è il codice dei dinosauri in questo mondo ~~ 2 anni
chrs

96

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: ho aggiunto una versione Swift 3 della mia risposta originale. In questa versione ho rimosso l'esempio di co-animazione mostrato nella versione Swift 2 in quanto sembra aver confuso molte persone.

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
C'è un motivo particolare per cui stai dicendo al vc di aggiornare la sua barra di stato? Questo sembra funzionare bene passando nilcome blocco di animazione.
Mike Sprague

2
È un esempio di qualcosa che potresti fare come animazione parallela (il commento immediatamente sopra indica che è opzionale). Anche passare nilè una cosa perfettamente valida da fare.
par

1
@par, dovresti essere più difensivo e chiamare il completamento quando transitionCoordinatorè nullo?
Aurelien Porte

@AurelienPorte È un'ottima presa e direi di sì, dovresti. Aggiornerò la risposta.
par

1
@cbowns Non ne sono sicuro al 100% perché non l'ho visto accadere, ma se non vedi un transitionCoordinator, è probabile che tu chiami questa funzione troppo presto nel ciclo di vita del controller di navigazione. Attendi almeno fino a quando viewWillAppear()viene chiamato prima di provare a eseguire il push di un controller di visualizzazione con animazione.
par

28

Sulla base della risposta di par (che era l'unica che funzionava con iOS9), ma più semplice e con un altro mancante (che avrebbe potuto portare al completamento mai chiamato):

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

Non funziona per me. Il TransitionCoordinator è nullo per me.
tcurdt

Per me va bene. Anche questo è meglio che accettato perché il completamento dell'animazione non è sempre lo stesso del completamento push.
Anton Plebanovich

Ti manca DispatchQueue.main.async per il caso non animato. Il contratto di questo metodo è che il gestore del completamento è chiamato in modo asincrono, non dovresti violarlo perché può portare a bug sottili.
Werner Altewischer

24

Attualmente il UINavigationControllernon lo supporta. Ma c'è UINavigationControllerDelegatequello che puoi usare.

Un modo semplice per ottenere ciò è creare sottoclassi UINavigationControllere aggiungere una proprietà del blocco di completamento:

@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

Prima di spingere il nuovo controller di visualizzazione dovresti impostare il blocco di completamento:

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

Questa nuova sottoclasse può essere assegnata in Interface Builder o essere utilizzata a livello di codice in questo modo:

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

8
L'aggiunta di un elenco di blocchi di completamento mappati ai controller di visualizzazione probabilmente lo renderebbe più utile, e un nuovo metodo, forse chiamato pushViewController:animated:completion:, renderebbe questa una soluzione elegante.
Iperbole

1
NB per il 2018 è davvero solo questo ... stackoverflow.com/a/43017103/294884
Fattie

8

Ecco la versione di Swift 4 con il 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()
    }
}

Nel caso qualcun altro ne abbia bisogno.


Se esegui un semplice test su questo, scoprirai che il blocco di completamento si attiva prima che l'animazione sia terminata. Quindi questo probabilmente non fornisce ciò che molti stanno cercando.
ferro di

7

Per espandere la risposta di @Klaas (e come risultato di questa domanda) ho aggiunto blocchi di completamento direttamente al metodo 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

Da utilizzare come segue:

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

Brillante. Grazie mille
Petar

if... (self.pushedVC == viewController) {non è corretto. È necessario testare l'uguaglianza tra gli oggetti utilizzando isEqual:, ad esempio,[self.pushedVC isEqual:viewController]
Evan R

@EvanR che è probabilmente più tecnicamente corretto sì. hai visto un errore nel confrontare le istanze in altro modo?
Sam

@ Sam non specificamente con questo esempio (non lo ha implementato) ma sicuramente nel testare l'uguaglianza con altri oggetti - vedere i documenti di Apple su questo: developer.apple.com/library/ios/documentation/General/… . Il tuo metodo di confronto funziona sempre in questo caso?
Evan R,

Non ho visto che non funziona o avrei cambiato la mia risposta. Per quanto ne so iOS non fa nulla di intelligente per ricreare i controller di visualizzazione come fa Android con le attività. ma sì, isEqualprobabilmente sarebbe tecnicamente più corretto se mai lo avessero fatto.
Sam

5

A partire da iOS 7.0, puoi utilizzare UIViewControllerTransitionCoordinatorper aggiungere un blocco di completamento push:

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
Non è esattamente la stessa cosa di UINavigationController push, pop, ecc.
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

Ci vuole un po 'più di pipe per aggiungere questo comportamento e mantenere la possibilità di impostare un delegato esterno.

Ecco un'implementazione documentata che mantiene la funzionalità delegato:

LBXCompletingNavigationController

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.