Bloque de terminación para popViewController

113

Al descartar un controlador de vista modal mediante dismissViewController, existe la opción de proporcionar un bloque de finalización. ¿Existe un equivalente similar para popViewController?

El argumento de la finalización es bastante útil. Por ejemplo, puedo usarlo para retrasar la eliminación de una fila de una vista de tabla hasta que el modal esté fuera de la pantalla, lo que permite al usuario ver la animación de la fila. Al regresar de un controlador de vista empujado, me gustaría tener la misma oportunidad.

He intentado colocarlo popViewControlleren un UIViewbloque de animación, donde tengo acceso a un bloque de finalización. Sin embargo, esto produce algunos efectos secundarios no deseados en la vista que se abre.

Si no existe tal método disponible, ¿cuáles son algunas soluciones?

Ben Packard
fuente
stackoverflow.com/a/33767837/2774520 Creo que esta es la forma más nativa
Oleksii Nezhyborets
3
Para 2018, esto es muy simple y estándar: stackoverflow.com/a/43017103/294884
Fattie

Respuestas:

199

Sé que se aceptó una respuesta hace más de dos años, sin embargo, esta respuesta está incompleta.

No hay forma de hacer lo que quiere de inmediato

Esto es técnicamente correcto porque la UINavigationControllerAPI no ofrece ninguna opción para esto. Sin embargo, al usar el marco CoreAnimation, es posible agregar un bloque de finalización a la animación subyacente:

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

[self.navigationController popViewControllerAnimated:YES];

[CATransaction commit];

El bloque de finalización se llamará tan pronto como popViewControllerAnimated:finalice la animación utilizada por . Esta funcionalidad está disponible desde iOS 4.

Joris Kluivers
fuente
5
Puse esto en una extensión de UINavigationController en Swift:extension UINavigationController { func popViewControllerWithHandler(handler: ()->()) { CATransaction.begin() CATransaction.setCompletionBlock(handler) self.popViewControllerAnimated(true) CATransaction.commit() } }
Arbitur
1
No parece funcionar para mí, cuando hago CompletionHandler en despedirViewController, la vista que lo presentaba es parte de la jerarquía de vistas. Cuando hago lo mismo con CATransaction, recibo una advertencia de que la vista no es parte de la jerarquía de vistas.
moger777
1
Bien, parece que funciona si invierte el bloque de inicio y finalización. Lo siento por el voto
negativo,
7
Sí, parecía que sería increíble, pero no parece funcionar (al menos en iOS 8). El bloque de finalización se llama de inmediato. Probablemente debido a la mezcla de animaciones centrales con animaciones de estilo UIView.
pegado el
5
ESTO NO FUNCIONA
durazno
51

Para la versión SWIFT de iOS9 : funciona como un encanto (no había probado para versiones anteriores). Basado en esta respuesta

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
fuente
No funcionará si no está animado, debe completarse en el siguiente ciclo de ejecución para hacerlo correctamente.
rshev
@rshev ¿por qué en el próximo runloop?
Ben Sinclair
@Andy, por lo que recuerdo haber experimentado con esto, algo aún no se había propagado en ese momento. Intente experimentar con él, me encantará saber cómo funciona para usted.
rshev
@rshev Creo que lo hice de la misma manera antes, tengo que verificarlo. Las pruebas actuales funcionan bien.
Ben Sinclair
1
@LanceSamaria Sugiero usar viewDidDisappear. Compruebe si la barra de navegación está disponible, de lo contrario, no se muestra en la barra de navegación, por lo que apareció. if (self.navigationController == nil) {desencadena tu acción}
HotJard
32

Hice una Swiftversión con extensiones con la respuesta de @JorisKluivers .

Esto llamará un cierre de finalización después de que se complete la animación para ambos pushy 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
fuente
Para mí, en iOS 8.4, escrito en ObjC, el bloque se dispara a la mitad de la animación. ¿Realmente se dispara en el momento adecuado si está escrito en Swift (8.4)?
Julian F. Weinert
El bloque de finalización de @Arbitur se llama después de llamar popViewControllero pushViewController, pero si comprueba qué es el topViewController justo después, notará que sigue siendo el anterior, como popo pushnunca sucedió ...
Bogdan Razvan
@BogdanRazvan ¿después de qué? ¿Se llama al cierre de finalización una vez que se completa la animación?
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
fuente
17

Tuve el mismo problema. Y como tuve que usarlo en múltiples ocasiones, y dentro de cadenas de bloques de finalización, creé esta solución genérica en una subclase 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];
}

Asumiendo

@interface NavigationController : UINavigationController <UINavigationControllerDelegate>

y

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

y

- (id) initWithRootViewController:(UIViewController *) rootViewController {
    self = [super initWithRootViewController:rootViewController];
    if (self) {
        self.delegate = self;
    }
    return self;
}
Jos Jong
fuente
1
Me gusta mucho esta solución, la voy a probar con una categoría y un objeto asociado.
spstanley
@spstanley necesitas publicar este pod :)
k06a
Versión Swift -> stackoverflow.com/a/60090678/4010725
WILL K.
15

No hay forma de hacer lo que desea de inmediato. es decir, no existe un método con un bloque de finalización para extraer un controlador de vista de una pila de navegación.

Lo que haría es poner la lógica viewDidAppear. Se llamará cuando la vista haya terminado de aparecer en pantalla. Se llamará para todos los escenarios diferentes del controlador de vista que aparecen, pero eso debería estar bien.

O puede usar el UINavigationControllerDelegatemétodo navigationController:didShowViewController:animated:para hacer algo similar. Esto se llama cuando el controlador de navegación ha terminado de presionar o abrir un controlador de vista.

Mattjgalloway
fuente
Intenté esto. Estaba almacenando una matriz de 'índices de fila eliminados' y cada vez que aparece la vista, verifico si es necesario eliminar algo. Rápidamente se volvió difícil de manejar, pero podría darle otra oportunidad. Me pregunto por qué Apple lo proporciona para una transición pero no para la otra.
Ben Packard
1
Es muy nuevo en el dismissViewController. Tal vez se recupere popViewController. Presentar un radar :-).
mattjgalloway
En serio, presente un radar. Es más probable que lo consiga si la gente lo solicita.
mattjgalloway
1
Ese es el lugar adecuado para solicitarlo. Hay una opción para que la clasificación sea 'Característica'.
Mattjgalloway
3
Esta respuesta no es del todo correcta. Si bien no puede configurar el bloque de nuevo estilo como activado -dismissViewController:animated:completionBlock:, puede obtener la animación a través del delegado del controlador de navegación. Una vez completada la animación, -navigationController:didShowViewController:animated:se llamará al delegado y podrá hacer lo que necesite allí mismo.
Jason Coco
13

Trabajar con o sin animación correctamente, y también incluye 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
fuente
¿Alguna razón en particular por la que llamas al completion()async?
leviatán
1
cuando la animación con coordinador completionnunca se ejecuta en el mismo runloop. esto garantiza que completionnunca se ejecute en el mismo runloop cuando no esté animando. es mejor no tener este tipo de inconsistencias.
rshev
11

Basado en la respuesta de @ HotJard, cuando todo lo que quieres son solo un par de líneas de código. Rapido y Facil.

Rápido 4 :

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

Para 2018 ...

si tienes esto ...

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

y desea agregar una finalización ...

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

Es así de simple.

Consejo útil ...

Es el mismo trato para la popToViewControllerllamada práctica .

Una cosa típica es que tiene una pila de incorporación de un trillón de pantallas. Cuando finalmente haya terminado, regrese a la pantalla "base" y, finalmente, inicie la aplicación.

Por lo tanto, en la pantalla "base", para "retroceder completamente", 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
fuente
5

El bloque de finalización se llama después de llamar al método viewDidDisappear en el controlador de vista presentado, por lo que poner código en el método viewDidDisappear del controlador de vista emergente debería funcionar igual que un bloque de finalización.

rdelmar
fuente
Claro, excepto que tienes que manejar todos los casos en los que la vista desaparece por alguna otra razón.
Ben Packard
1
@BenPackard, sí, y lo mismo es cierto para ponerlo en viewDidAppear en la respuesta que aceptó.
rdelmar
5

Respuesta rápida 3, gracias a esta respuesta: 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
fuente
4

Versión Swift 4 con el parámetro viewController opcional para pasar a uno específico.

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
fuente
La respuesta aceptada parece funcionar en mi entorno de desarrollo con todos los emuladores / dispositivos que tengo, pero todavía recibo informes de errores de los usuarios de producción. No estoy seguro de si esto resolverá el problema de producción, pero permítame votarlo solo para que alguien pueda intentarlo si obtiene el mismo problema de la respuesta aceptada.
Sean
4

Se limpió la versión de Swift 4 basada en esta respuesta .

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
fuente
2

2020 Swift 5.1 forma

Esta solución garantiza que la finalización se ejecute después de que popViewController esté completamente terminado. Puede probarlo haciendo otra operación en NavigationController al finalizar: en todas las demás soluciones anteriores, UINavigationController todavía está ocupado con la operación popViewController y no responde.

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.
fuente
1

Solo para completar, he creado una categoría Objective-C lista para usar:

// 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
fuente
1

Logré exactamente esto con precisión usando un bloque. Quería que mi controlador de resultados obtenido mostrara la fila que fue agregada por la vista modal, solo una vez que haya abandonado completamente la pantalla, para que el usuario pueda ver el cambio. En preparación para segue, que es responsable de mostrar el controlador de vista modal, configuro el bloque que quiero ejecutar cuando el modal desaparece. Y en el controlador de vista modal anulo viewDidDissapear y luego llamo al bloque. Simplemente comienzo las actualizaciones cuando va a aparecer el modal y finalizo las actualizaciones cuando desaparece, pero eso se debe a que estoy usando un NSFetchedResultsController, sin embargo, puede hacer lo que quiera dentro del bloque.

-(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
fuente
1

Use la siguiente extensión en su código: (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
fuente