Bloc de complétion pour popViewController

114

Lors du rejet d'un contrôleur de vue modale à l'aide de dismissViewController, il est possible de fournir un bloc de saisie semi-automatique. Existe-t-il un équivalent similaire pour popViewController?

L'argument de l'achèvement est assez pratique. Par exemple, je peux l'utiliser pour suspendre la suppression d'une ligne d'une vue de table jusqu'à ce que le modal soit hors écran, permettant à l'utilisateur de voir l'animation de ligne. En revenant d'un contrôleur de vue poussé, j'aimerais avoir la même opportunité.

J'ai essayé de placer popViewControllerdans un UIViewbloc d'animation, où j'ai accès à un bloc de complétion. Cependant, cela produit des effets secondaires indésirables sur la vue sur laquelle on passe.

Si aucune méthode de ce type n'est disponible, quelles sont les solutions de contournement?

Ben Packard
la source
stackoverflow.com/a/33767837/2774520 Je pense que cette façon est la plus native
Oleksii Nezhyborets
3
Pour 2018, c'est très simple et standard: stackoverflow.com/a/43017103/294884
Fattie

Réponses:

200

Je sais qu'une réponse a été acceptée il y a plus de deux ans, mais cette réponse est incomplète.

Il n'y a aucun moyen de faire ce que vous voulez hors de la boîte

Ceci est techniquement correct car l' UINavigationControllerAPI n'offre aucune option pour cela. Cependant, en utilisant le framework CoreAnimation, il est possible d'ajouter un bloc de complétion à l'animation sous-jacente:

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

[self.navigationController popViewControllerAnimated:YES];

[CATransaction commit];

Le bloc de complétion sera appelé dès que l'animation utilisée par popViewControllerAnimated:se termine. Cette fonctionnalité est disponible depuis iOS 4.

Joris Kluivers
la source
5
J'ai mis cela dans une extension de UINavigationController dans Swift:extension UINavigationController { func popViewControllerWithHandler(handler: ()->()) { CATransaction.begin() CATransaction.setCompletionBlock(handler) self.popViewControllerAnimated(true) CATransaction.commit() } }
Arbitur
1
Cela ne semble pas fonctionner pour moi, lorsque je fais completionHandler sur ignoreViewController, la vue qui la présentait fait partie de la hiérarchie des vues. Lorsque je fais la même chose avec CATransaction, je reçois un avertissement indiquant que la vue ne fait pas partie de la hiérarchie des vues.
moger777
1
OK, ressemble à vos travaux si vous inversez le bloc de début et de fin. Désolé pour le vote vers le bas, mais le débordement de pile ne me permettra pas de changer :(
moger777
7
Oui, cela semblait être génial, mais cela ne semble pas fonctionner (du moins sur iOS 8). Le bloc d'achèvement est appelé immédiatement. Probablement à cause du mélange d'animations de base avec des animations de style UIView.
stuckj
5
CELA NE FONCTIONNE PAS
durazno
51

Pour la version iOS9 SWIFT - fonctionne comme un charme (n'avait pas été testé pour les versions antérieures). Basé sur cette réponse

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()
        }
    }
}
HotJard
la source
Ne fonctionnera pas s'il n'est pas animé, devrait effectuer la complétion sur la prochaine boucle d'exécution pour le faire correctement.
rshev
@rshev pourquoi sur le prochain runloop?
Ben Sinclair
@Andy d'après ce dont je me souviens avoir expérimenté cela, quelque chose n'avait pas encore été propagé à ce stade. Essayez de l'expérimenter, aimez savoir comment cela fonctionne pour vous.
rshev
@rshev Je pense que je l'ai eu de la même manière avant, je dois vérifier. Les tests actuels fonctionnent correctement.
Ben Sinclair
1
@LanceSamaria Je suggère d'utiliser viewDidDisappear. Vérifiez si la barre de navigation est disponible, sinon - elle n'est pas affichée dans la barre de navigation, elle a donc été sautée. if (self.navigationController == nil) {déclenche votre action}
HotJard
32

J'ai fait une Swiftversion avec des extensions avec la réponse @JorisKluivers .

Cela appellera une fermeture d'achèvement une fois l'animation terminée pour pushet 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()
    }
}
Arbitur
la source
Pour moi, dans iOS 8.4, écrit en ObjC, le bloc se déclenche à mi-chemin de l'animation. Cela se déclenche-t-il vraiment au bon moment s'il est écrit en Swift (8.4)?
Julian F. Weinert
Le bloc de complétion @Arbitur est en effet appelé après avoir appelé popViewControllerou pushViewController, mais si vous vérifiez ce qu'est le topViewController juste après, vous remarquerez qu'il s'agit toujours de l'ancien, comme popou pushne s'est jamais produit ...
Bogdan Razvan
@BogdanRazvan juste après quoi? Votre clôture de fin est-elle appelée une fois l'animation terminée?
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()
}
}
Muhammad Waqas
la source
17

J'ai eu le même problème. Et parce que j'ai dû l'utiliser à plusieurs reprises, et dans des chaînes de blocs d'achèvement, j'ai créé cette solution générique dans une sous-classe 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];
}

En supposant

@interface NavigationController : UINavigationController <UINavigationControllerDelegate>

et

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

et

- (id) initWithRootViewController:(UIViewController *) rootViewController {
    self = [super initWithRootViewController:rootViewController];
    if (self) {
        self.delegate = self;
    }
    return self;
}
Jos Jong
la source
1
J'aime beaucoup cette solution, je vais l'essayer avec une catégorie et un objet associé.
spstanley
@spstanley vous devez publier ce pod :)
k06a
Version Swift -> stackoverflow.com/a/60090678/4010725
WILL K.
15

Il n'y a aucun moyen de faire ce que vous voulez tout de suite. c'est-à-dire qu'il n'y a pas de méthode avec un bloc de complétion pour faire sauter un contrôleur de vue à partir d'une pile de navigation.

Ce que je ferais, c'est mettre la logique dans viewDidAppear . Cela sera appelé lorsque la vue aura fini de s'afficher à l'écran. Il sera appelé pour tous les différents scénarios d'apparition du contrôleur de vue, mais cela devrait être bien.

Ou vous pouvez utiliser la UINavigationControllerDelegateméthode navigationController:didShowViewController:animated:pour faire une chose similaire. Ceci est appelé lorsque le contrôleur de navigation a fini de pousser ou de faire sauter un contrôleur de vue.

mattjgalloway
la source
J'ai essayé cela. Je stockais un tableau d '«index de lignes supprimés» et chaque fois que la vue apparaît, je vérifiais si quelque chose devait être supprimé. Il est rapidement devenu difficile à manier, mais je pourrais peut-être lui donner une autre chance. Je me demande pourquoi Apple le propose pour une transition mais pas pour l'autre.
Ben Packard
1
Ce n'est que très nouveau sur le dismissViewController. Peut-être que cela arrivera popViewController. Déposer un radar :-).
mattjgalloway
Sérieusement cependant, déposez un radar. Il est plus susceptible de réussir si les gens le demandent.
mattjgalloway
1
C'est le bon endroit pour le demander. Il existe une option pour que la classification soit «Feature».
mattjgalloway
3
Cette réponse n'est pas complètement correcte. Bien que vous ne puissiez pas définir le bloc de nouveau style comme activé -dismissViewController:animated:completionBlock:, vous pouvez obtenir l'animation via le délégué du contrôleur de navigation. Une fois l'animation terminée, -navigationController:didShowViewController:animated:le délégué sera appelé et vous pourrez faire tout ce dont vous avez besoin sur place.
Jason Coco
13

Travailler correctement avec ou sans animation, et comprend également 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)
  }
}
rshev
la source
Une raison particulière pour laquelle vous appelez l' completion()async?
leviathan
1
lorsque l'animation avec le coordinateur completionn'est jamais exécutée sur la même boucle d'exécution. cela garantit de completionne jamais fonctionner sur la même boucle d'exécution lorsqu'il n'est pas animé. il vaut mieux ne pas avoir ce genre d'incohérence.
rshev
11

Basé sur la réponse de @ HotJard, quand tout ce que vous voulez, c'est juste quelques lignes de code. Rapide et facile.

Swift 4 :

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

Pour 2018 ...

si vous avez ça ...

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

et vous souhaitez ajouter une complétion ...

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

c'est si simple.

Astuce pratique ...

C'est la même chose pour le pratique popToViewController appel .

Une chose typique est que vous avez une pile d'intégration d'un million d'écrans. Une fois terminé, vous revenez à votre écran "de base", puis lancez enfin l'application.

Donc, dans l’écran "de base", pour aller "tout en arrière", 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()
}
Fattie
la source
5

Le bloc de complétion est appelé après que la méthode viewDidDisappear a été appelée sur le contrôleur de vue présenté, donc mettre du code dans la méthode viewDidDisappear du contrôleur de vue popped devrait fonctionner de la même manière qu'un bloc de complétion.

rdelmar
la source
Bien sûr, sauf que vous devez gérer tous les cas où la vue disparaît pour une autre raison.
Ben Packard
1
@BenPackard, oui, et il en va de même pour le mettre dans viewDidAppear dans la réponse que vous avez acceptée.
rdelmar
5

Réponse Swift 3, grâce à cette réponse: 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()
    }
}
Benobab
la source
4

Version Swift 4 avec paramètre optionnel viewController pour accéder à un paramètre spécifique.

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()
        }
    }
}
TejAces
la source
La réponse acceptée semble fonctionner dans mon environnement de développement avec tous les émulateurs / périphériques que j'ai, mais je reçois toujours des bogues signalés par les utilisateurs de production. Je ne sais pas si cela résoudra le problème de production, mais permettez-moi de voter pour que quelqu'un puisse l'essayer si la réponse acceptée pose le même problème.
Sean
4

Nettoyé la version Swift 4 en fonction de cette réponse .

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()
        }
    }
}
d4Rk
la source
2

Il existe un pod appelé UINavigationControllerWithCompletionBlock qui ajoute la prise en charge d'un bloc d'achèvement lors de la poussée et de l'affichage d'un UINavigationController.

duncanc4
la source
2

2020 Swift 5.1 voie

Cette solution garantit que l'achèvement est exécuté une fois que popViewController est complètement terminé. Vous pouvez le tester en effectuant une autre opération sur le NavigationController à la fin: Dans toutes les autres solutions ci-dessus, UINavigationController est toujours occupé avec l'opération popViewController et ne répond pas.

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)
    }
}
WILL K.
la source
1

Pour être complet, j'ai préparé une catégorie Objective-C prête à l'emploi:

// 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
Diego Freniche
la source
1

J'ai réalisé exactement cela avec précision en utilisant un bloc. Je voulais que mon contrôleur de résultats récupéré affiche la ligne qui a été ajoutée par la vue modale, seulement une fois qu'elle a complètement quitté l'écran, afin que l'utilisateur puisse voir le changement en cours. En préparation pour segue qui est chargé d'afficher le contrôleur de vue modale, j'ai défini le bloc que je souhaite exécuter lorsque le modal disparaît. Et dans le contrôleur de vue modale, je remplace viewDidDissapear puis j'appelle le bloc. Je commence simplement les mises à jour lorsque le modal va apparaître et met fin aux mises à jour lorsqu'il disparaît, mais c'est parce que j'utilise un NSFetchedResultsController, mais vous pouvez faire ce que vous voulez à l'intérieur du bloc.

-(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
malhal
la source
1

Utilisez l'extension suivante sur votre code: (Swift 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()
    }
}
Rigoberto Sáenz Imbacuán
la source