Manipulador de conclusão para UINavigationController “pushViewController: animado”?

110

Estou prestes a criar um aplicativo usando um UINavigationControllerpara apresentar os próximos controladores de visualização. Com o iOS5, há um novo método de apresentação UIViewControllers:

presentViewController:animated:completion:

Agora me pergunto por que não existe um manipulador de conclusão para UINavigationController? Há apenas

pushViewController:animated:

É possível criar meu próprio manipulador de conclusão como o novo presentViewController:animated:completion:?

Geforce
fonte
2
não é exatamente a mesma coisa que um manipulador de conclusão, mas viewDidAppear:animated:permite que você execute o código cada vez que seu controlador de visualização aparecer na tela ( viewDidLoadapenas a primeira vez que seu controlador de visualização é carregado)
Moxy
@Moxy, você quer dizer-(void)viewDidAppear:(BOOL)animated
George
2
para 2018 ... na verdade, é só isso: stackoverflow.com/a/43017103/294884
Fattie

Respostas:

139

Veja a resposta da par para outra solução mais atualizada

UINavigationControlleranimações são executadas com CoreAnimation, portanto, faria sentido encapsular o código dentro CATransactione, assim, definir um bloco de conclusão.

Swift :

Para o rápido, sugiro criar uma extensão como tal

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

Cabeçalho:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

Implementação:

#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
fonte
1
Eu acredito (não testei) que isso poderia fornecer resultados imprecisos se o controlador de exibição apresentado disparar animações dentro de suas implementações viewDidLoad ou viewWillAppear. Acho que essas animações serão iniciadas antes de pushViewController: animated: Returns - portanto, o manipulador de conclusão não será chamado até que as animações recém-disparadas tenham terminado.
Matt H.
1
@MattH. Fiz alguns testes esta noite e parece que ao usar pushViewController:animated:ou popViewController:animated, as chamadas viewDidLoade viewDidAppearacontecem em ciclos de execução subsequentes. Portanto, minha impressão é que mesmo que esses métodos invoquem animações, eles não farão parte da transação fornecida no exemplo de código. Era essa a sua preocupação? Porque essa solução é incrivelmente simples.
LeffelMania
1
Olhando para trás, para esta questão, acho que em geral as preocupações mencionadas por @MattH. e @LeffelMania destacam um problema válido com esta solução - em última análise, assume que a transação será concluída após a conclusão do push, mas a estrutura não garante esse comportamento. É garantido que o controlador de visualização em questão é mostrado didShowViewController. Embora esta solução seja fantasticamente simples, eu questionaria sua "compatibilidade com o futuro". Especialmente devido às mudanças para visualizar os retornos de chamada do ciclo de vida que vieram com ios7 / 8
Sam,
8
Isso não parece funcionar de maneira confiável em dispositivos iOS 9. Veja minhas respostas de ou @par abaixo para uma alternativa
Mike Sprague
1
@ZevEisenberg definitivamente. Minha resposta é o código de dinossauro neste mundo ~~ 2 anos
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: Eu adicionei uma versão Swift 3 da minha resposta original. Nesta versão, removi o exemplo de co-animação mostrado na versão Swift 2, pois parece ter confundido muitas pessoas.

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
fonte
1
Existe um motivo específico pelo qual você está dizendo ao vc para atualizar sua barra de status? Isso parece funcionar bem, passando nilcomo bloco de animação.
Mike Sprague
2
É um exemplo de algo que você pode fazer como uma animação paralela (o comentário imediatamente acima indica que é opcional). Passar também nilé uma coisa perfeitamente válida.
par
1
@par, você deveria ser mais defensivo e chamar a conclusão quando o transitionCoordinatorfor nulo?
Aurelien Porte
@AurelienPorte Essa é uma ótima pegada e eu diria que sim, você deveria. Vou atualizar a resposta.
par
1
@cbowns Não tenho 100% de certeza sobre isso, pois não vi isso acontecer, mas se você não transitionCoordinatorvir um , é provável que esteja chamando essa função muito cedo no ciclo de vida do controlador de navegação. Espere pelo menos até viewWillAppear()ser chamado antes de tentar empurrar um controlador de visualização com animação.
par
28

Com base na resposta do par (que era a única que funcionava com iOS9), mas mais simples e com um else ausente (o que poderia fazer com que a conclusão nunca fosse chamada):

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
fonte
Nao funciona para mim. O TransitionCoordinator é nulo para mim.
tcurdt
Funciona para mim. Além disso, este é melhor do que aceito porque a conclusão da animação nem sempre é igual à conclusão do push.
Anton Plebanovich,
Está faltando um DispatchQueue.main.async para o caso não animado. O contrato desse método é que o manipulador de conclusão seja chamado de forma assíncrona, você não deve violar isso porque pode levar a bugs sutis.
Werner Altewischer
24

Atualmente, o UINavigationControllernão suporta isso. Mas há o UINavigationControllerDelegateque você pode usar.

Uma maneira fácil de fazer isso é subclassificar UINavigationControllere adicionar uma propriedade de bloco de conclusão:

@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

Antes de enviar o novo controlador de visualização, você teria que definir o bloco de conclusão:

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

Esta nova subclasse pode ser atribuída no Interface Builder ou ser usada programaticamente desta forma:

PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];
Klaas
fonte
8
Adicionar uma lista de blocos de conclusão mapeados para controladores de exibição provavelmente tornaria isso mais útil, e um novo método, talvez chamado pushViewController:animated:completion:, tornaria isso uma solução elegante.
Hipérbole de
1
NB para 2018 é realmente apenas isso ... stackoverflow.com/a/43017103/294884
Fattie
8

Aqui está a versão do Swift 4 com o 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()
    }
}

Apenas no caso de alguém precisar disso.

François Nadeau
fonte
Se você executar um teste simples sobre isso, verá que o bloco de conclusão dispara antes de a animação terminar. Portanto, isso provavelmente não fornece o que muitos estão procurando.
ferradura
7

Para expandir a resposta de @Klaas (e como resultado desta pergunta), adicionei blocos de conclusão diretamente ao método 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

Para ser usado da seguinte forma:

UIViewController *vc = ...;
[(PbNavigationController *)self.navigationController pushViewController:vc animated:YES completion:^ {
    NSLog(@"COMPLETED");
}];
Sam
fonte
Brilhante. Muito obrigado
Petar
if... (self.pushedVC == viewController) {está incorreto. Você precisa testar a igualdade entre os objetos usando isEqual:, ou seja,[self.pushedVC isEqual:viewController]
Evan R
@EvanR que é provavelmente mais tecnicamente correto sim. você viu um erro ao comparar as instâncias de outra maneira?
Sam de
@Sam não especificamente com este exemplo (não o implementou), mas definitivamente testando a igualdade com outros objetos - veja a documentação da Apple sobre isso: developer.apple.com/library/ios/documentation/General/… . O seu método de comparação sempre funciona neste caso?
Evan R
Não vi que não funcionasse ou teria mudado minha resposta. Pelo que eu sei, o iOS não faz nada inteligente para recriar controladores de visualização como o Android faz com as atividades. mas sim, isEqualprovavelmente seria mais tecnicamente correto caso eles já fizessem.
Sam
5

Desde iOS 7.0, você pode usar UIViewControllerTransitionCoordinatorpara adicionar um bloco de conclusão 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
fonte
1
Isso não é exatamente a mesma coisa 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
fonte
2

É necessário um pouco mais de tubulação para adicionar esse comportamento e manter a capacidade de definir um delegado externo.

Esta é uma implementação documentada que mantém a funcionalidade de delegado:

LBXCompletingNavigationController

Nzeltzer
fonte