Gestionnaire d'achèvement pour UINavigationController «pushViewController: animated»?

110

Je suis sur le point de créer une application en utilisant un UINavigationControllerpour présenter les prochains contrôleurs de vue. Avec iOS5, il existe une nouvelle méthode pour présenter UIViewControllers:

presentViewController:animated:completion:

Maintenant, je me demande pourquoi n'y a-t-il pas de gestionnaire de complétion pour UINavigationController? Il y a juste

pushViewController:animated:

Est-il possible de créer mon propre gestionnaire de complétion comme le nouveau presentViewController:animated:completion:?

Geforce
la source
2
pas exactement la même chose qu'un gestionnaire de complétion, mais viewDidAppear:animated:vous permet d'exécuter du code à chaque fois que votre contrôleur de vue apparaît à l'écran ( viewDidLoadseulement la première fois que votre contrôleur de vue est chargé)
Moxy
@Moxy, voulez-vous dire-(void)viewDidAppear:(BOOL)animated
George
2
pour 2018 ... vraiment c'est juste ceci: stackoverflow.com/a/43017103/294884
Fattie

Réponses:

139

Voir la réponse de par pour une autre solution plus à jour

UINavigationControllerles animations sont exécutées avec CoreAnimation, il serait donc logique d'encapsuler le code à l'intérieur CATransactionet ainsi de définir un bloc de complétion.

Swift :

Pour swift, je suggère de créer une extension en tant que telle

extension UINavigationController {

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

}

Usage:

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

Objectif c

Entête:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

La mise en oeuvre:

#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
la source
1
Je crois (je n'ai pas testé) que cela pourrait fournir des résultats inexacts si le contrôleur de vue présenté déclenche des animations dans ses implémentations viewDidLoad ou viewWillAppear. Je pense que ces animations seront lancées avant pushViewController: animated: Returns - ainsi, le gestionnaire d'achèvement ne sera pas appelé tant que les animations nouvellement déclenchées ne seront pas terminées.
Matt H.20
1
@MattH. J'ai fait quelques tests ce soir et il semble que lors de l'utilisation de pushViewController:animated:ou popViewController:animated, les appels viewDidLoadet viewDidAppearse produisent dans les cycles de boucle d'exécution suivants. J'ai donc l'impression que même si ces méthodes invoquent des animations, elles ne feront pas partie de la transaction fournie dans l'exemple de code. Était-ce votre souci? Parce que cette solution est fabuleusement simple.
LeffelMania
1
En repensant à cette question, je pense en général aux préoccupations évoquées par @MattH. et @LeffelMania mettent en évidence un problème valide avec cette solution - cela suppose finalement que la transaction sera terminée une fois la poussée terminée, mais le cadre ne garantit pas ce comportement. Il est garanti que le contrôleur de vue en question est affiché didShowViewController. Bien que cette solution soit incroyablement simple, je remettrais en question sa "pérennité". Surtout compte tenu des changements pour afficher les rappels de cycle de vie fournis avec ios7 / 8
Sam
8
Cela ne semble pas fonctionner de manière fiable sur les appareils iOS 9. Voir les réponses de my ou @ par ci-dessous pour une alternative
Mike Sprague
1
@ZevEisenberg définitivement. Ma réponse est le code des dinosaures dans ce monde ~~ 2 ans
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: J'ai ajouté une version Swift 3 de ma réponse originale. Dans cette version, j'ai supprimé l'exemple de co-animation montré dans la version Swift 2 car il semble avoir dérouté beaucoup de gens.

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
la source
1
Y a-t-il une raison particulière pour laquelle vous dites au vc de mettre à jour sa barre d'état? Cela semble bien fonctionner en passant nilen tant que bloc d'animation.
Mike Sprague
2
C'est un exemple de quelque chose que vous pourriez faire en tant qu'animation parallèle (le commentaire immédiatement au-dessus indique que c'est facultatif). Passer nilest une chose parfaitement valable à faire aussi.
par
1
@par, Devriez-vous être plus défensif et appeler la complétion quand le transitionCoordinatorest nul?
Aurelien Porte
@AurelienPorte C'est une belle prise et je dirais que oui, vous devriez. Je mettrai à jour la réponse.
par
1
@cbowns Je ne suis pas sûr à 100% à ce sujet car je n'ai pas vu cela se produire, mais si vous ne voyez pas de, transitionCoordinatoril est probable que vous appeliez cette fonction trop tôt dans le cycle de vie du contrôleur de navigation. Attendez au moins jusqu'à ce que viewWillAppear()soit appelé avant d'essayer de pousser un contrôleur de vue avec une animation.
par
28

Basé sur la réponse de par (qui était la seule qui fonctionnait avec iOS9), mais plus simple et avec un autre manquant (ce qui aurait pu conduire à ce que l'achèvement ne soit jamais appelé):

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
la source
Ça ne marche pas pour moi. Le transitionCoordinator est nul pour moi.
tcurdt
Travaille pour moi. De plus, celui-ci est meilleur que celui accepté car la complétion de l'animation n'est pas toujours la même que la complétion par poussée.
Anton Plebanovich
Il vous manque un DispatchQueue.main.async pour le cas non animé. Le contrat de cette méthode est que le gestionnaire d'achèvement est appelé de manière asynchrone, vous ne devez pas violer cela car cela peut conduire à des bogues subtils.
Werner Altewischer le
24

Actuellement, le UINavigationControllerne prend pas en charge cela. Mais il y a leUINavigationControllerDelegate que vous pouvez utiliser.

Un moyen simple d'y parvenir est de sous UINavigationController- classer et d'ajouter une propriété de bloc de complétion:

@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

Avant de pousser le nouveau contrôleur de vue, vous devez définir le bloc de complétion:

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

Cette nouvelle sous-classe peut être attribuée dans Interface Builder ou être utilisée par programme comme ceci:

PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];
Klaas
la source
8
Ajouter une liste de blocs de complétion mappés sur des contrôleurs de vue rendrait probablement cela très utile, et une nouvelle méthode, peut-être appelée, en pushViewController:animated:completion:ferait une solution élégante.
Hyperbole
1
NB pour 2018, c'est vraiment juste ça ... stackoverflow.com/a/43017103/294884
Fattie
8

Voici la version Swift 4 avec le 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()
    }
}

Juste au cas où quelqu'un d'autre en aurait besoin.

François Nadeau
la source
Si vous exécutez un test simple à ce sujet, vous constaterez que le bloc d'achèvement se déclenche avant la fin de l'animation. Donc, cela ne fournit probablement pas ce que beaucoup recherchent.
horseshoe 7
7

Pour développer la réponse de @Klaas (et à la suite de cette question), j'ai ajouté des blocs de complétion directement à la méthode 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

A utiliser comme suit:

UIViewController *vc = ...;
[(PbNavigationController *)self.navigationController pushViewController:vc animated:YES completion:^ {
    NSLog(@"COMPLETED");
}];
Sam
la source
Brillant. Merci beaucoup
Petar
if... (self.pushedVC == viewController) {est incorrect. Vous devez tester l'égalité entre les objets en utilisant isEqual:, par exemple,[self.pushedVC isEqual:viewController]
Evan R
@EvanR qui est probablement plus techniquement correct, oui. avez-vous vu une erreur en comparant les instances dans l'autre sens?
Sam le
@Sam pas spécifiquement avec cet exemple (ne l'a pas implémenté) mais certainement en testant l'égalité avec d'autres objets - voir la documentation d'Apple à ce sujet: developer.apple.com/library/ios/documentation/General/… . Votre méthode de comparaison fonctionne-t-elle toujours dans ce cas?
Evan R
Je n'ai pas vu que cela ne fonctionnait pas ou j'aurais changé ma réponse. Pour autant que je sache, iOS ne fait rien d'intelligent pour recréer des contrôleurs de vue comme le fait Android avec des activités. mais oui, isEqualserait probablement plus techniquement correct au cas où ils l'auraient jamais fait.
Sam
5

Depuis iOS 7.0, vous pouvez utiliser UIViewControllerTransitionCoordinatorpour ajouter un bloc d'achèvement 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");
}];
wj2061
la source
1
Ce n'est pas tout à fait la même chose que UINavigationController push, pop, etc.
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
la source
2

Il faut un peu plus de travail pour ajouter ce comportement et conserver la possibilité de définir un délégué externe.

Voici une implémentation documentée qui maintient la fonctionnalité de délégué:

LBXCompletingNavigationController

nzeltzer
la source