¿Manejador de finalización para UINavigationController “pushViewController: animated”?

110

Estoy a punto de crear una aplicación usando un UINavigationControllerpara presentar los siguientes controladores de vista. Con iOS5 hay un nuevo método de presentación UIViewControllers:

presentViewController:animated:completion:

Ahora me pregunto por qué no hay un controlador de finalización para UINavigationController. Hay solo

pushViewController:animated:

¿Es posible crear mi propio controlador de finalización como el nuevo presentViewController:animated:completion:?

geforce
fuente
2
no es exactamente lo mismo que un controlador de finalización, pero le viewDidAppear:animated:permite ejecutar código cada vez que su controlador de vista aparece en la pantalla ( viewDidLoadsolo la primera vez que se carga su controlador de vista)
Moxy
@Moxy, quieres decir-(void)viewDidAppear:(BOOL)animated
George
2
para 2018 ... realmente es solo esto: stackoverflow.com/a/43017103/294884
Fattie

Respuestas:

139

Vea la respuesta de par para otra solución más actualizada

UINavigationControllerLas animaciones se ejecutan con CoreAnimation, por lo que tendría sentido encapsular el código dentro CATransactiony así establecer un bloque de finalización.

Rápido :

Para ser rápido, sugiero crear una extensión 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
}

C objetivo

Encabezamiento:

#import <UIKit/UIKit.h>

@interface UINavigationController (CompletionHandler)

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

@end

Implementación:

#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
fuente
1
Creo (no lo he probado) que esto podría proporcionar resultados inexactos si el controlador de vista presentado activa animaciones dentro de sus implementaciones viewDidLoad o viewWillAppear. Creo que esas animaciones se iniciarán antes de pushViewController: animated: returns; por lo tanto, no se llamará al controlador de finalización hasta que las animaciones recién activadas hayan terminado.
Matt H.
1
@MattH. Hice un par de pruebas esta noche y parece que cuando se usa pushViewController:animated:o popViewController:animated, las llamadas viewDidLoady viewDidAppearocurren en ciclos de ejecución posteriores. Entonces, mi impresión es que incluso si esos métodos invocan animaciones, no serán parte de la transacción proporcionada en el ejemplo de código. ¿Era ésa tu preocupación? Porque esta solución es fabulosamente simple.
LeffelMania
1
Mirando hacia atrás en esta pregunta, creo que en general las preocupaciones mencionadas por @MattH. y @LeffelMania resaltan un problema válido con esta solución: en última instancia, asume que la transacción se completará después de que se complete el envío, pero el marco no garantiza este comportamiento. Sin didShowViewControllerembargo, está garantizado que el controlador de vista en cuestión se muestra . Si bien esta solución es fantásticamente simple, cuestionaría su "capacidad para el futuro". Especialmente dados los cambios para ver las devoluciones de llamada del ciclo de vida que vinieron con ios7 / 8
Sam
8
Esto no parece funcionar de manera confiable en dispositivos iOS 9. Vea las respuestas de mi o @ par a continuación para obtener una alternativa
Mike Sprague
1
@ZevEisenberg definitivamente. Mi respuesta es código de dinosaurio en este mundo ~~ 2 años de edad
chrs
95

iOS 7+ Swift

Rápido 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() }
    }
}

EDITAR: agregué una versión Swift 3 de mi respuesta original. En esta versión, eliminé la co-animación de ejemplo que se muestra en la versión Swift 2, ya que parece haber confundido a mucha gente.

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

Rápido 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
fuente
1
¿Hay alguna razón en particular por la que le está diciendo al vc que actualice su barra de estado? Esto parece funcionar bien pasando nilcomo bloque de animación.
Mike Sprague
2
Es un ejemplo de algo que podría hacer como una animación paralela (el comentario inmediatamente arriba indica que es opcional). Pasar niles algo perfectamente válido para hacer también.
par
1
@par, ¿Deberías estar más a la defensiva y llamar a la finalización cuando el resultado transitionCoordinatores nulo?
Aurelien Porte
@AurelienPorte Esa es una gran captura y yo diría que sí, que deberías. Actualizaré la respuesta.
par
1
@cbowns No estoy 100% seguro de esto ya que no he visto que esto suceda, pero si no ve un transitionCoordinator, es probable que esté llamando a esta función demasiado pronto en el ciclo de vida del controlador de navegación. Espere al menos hasta que viewWillAppear()se llame antes de intentar presionar un controlador de vista con animación.
par
28

Basado en la respuesta de par (que fue la única que funcionó con iOS9), pero más simple y con un else (lo que podría haber llevado a que nunca se llamara a la finalización):

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
fuente
No me funciona. El coordinador de transición es nulo para mí.
tcurdt
Funciona para mi. Además, este es mejor que el aceptado porque la finalización de la animación no siempre es lo mismo que la finalización push.
Anton Plebanovich
Falta un DispatchQueue.main.async para el caso no animado. El contrato de este método es que el controlador de finalización se llama de forma asincrónica, no debe violar esto porque puede provocar errores sutiles.
Werner Altewischer
24

Actualmente, el UINavigationControllerno admite esto. Pero está el UINavigationControllerDelegateque puedes usar.

Una manera fácil de lograr esto es subclasificar UINavigationControllery agregar una propiedad de bloque de finalización:

@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 presionar el nuevo controlador de vista, tendría que establecer el bloque de finalización:

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

Esta nueva subclase puede asignarse en Interface Builder o usarse programáticamente de esta manera:

PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];
Klaas
fuente
8
Agregar una lista de bloques de finalización mapeados para ver los controladores probablemente lo haría más útil, y un nuevo método, tal vez llamado, pushViewController:animated:completion:lo convertiría en una solución elegante.
Hipérbole
1
NB para 2018 es realmente solo esto ... stackoverflow.com/a/43017103/294884
Fattie
8

Aquí está la versión Swift 4 con 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()
    }
}

En caso de que alguien más lo necesite.

Francois Nadeau
fuente
Si realiza una prueba simple sobre esto, encontrará que el bloque de finalización se activa antes de que finalice la animación. Así que probablemente esto no proporcione lo que muchos buscan.
herradura 7
7

Para ampliar la respuesta de @Klaas (y como resultado de esta pregunta) agregué bloques de finalización directamente al 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 utilizado de la siguiente manera:

UIViewController *vc = ...;
[(PbNavigationController *)self.navigationController pushViewController:vc animated:YES completion:^ {
    NSLog(@"COMPLETED");
}];
Sam
fuente
Brillante. Muchas gracias
Petar
if... (self.pushedVC == viewController) {Es incorrecto. Necesita probar la igualdad entre objetos usando isEqual:, es decir,[self.pushedVC isEqual:viewController]
Evan R
@EvanR eso es probablemente más técnicamente correcto, sí. ¿Ha visto un error al comparar las instancias al revés?
Sam
@Sam no específicamente con este ejemplo (no lo implementó) pero definitivamente al probar la igualdad con otros objetos — vea los documentos de Apple sobre esto: developer.apple.com/library/ios/documentation/General/… . ¿Su método de comparación siempre funciona en este caso?
Evan R
No he visto que no funcione o habría cambiado mi respuesta. Hasta donde yo sé, iOS no hace nada inteligente para recrear controladores de vista como lo hace Android con las actividades. pero sí, isEqualprobablemente sería más técnicamente correcto en caso de que alguna vez lo hicieran.
Sam
5

Desde iOS 7.0, puede usar UIViewControllerTransitionCoordinatorpara agregar un bloque de finalización de inserción:

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
fuente
1
Esto no es exactamente lo mismo 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
fuente
2

Se necesita un poco más de trabajo para agregar este comportamiento y conservar la capacidad de establecer un delegado externo.

Aquí hay una implementación documentada que mantiene la funcionalidad de delegado:

LBXCompletingNavigationController

nzeltzer
fuente