Blocco di completamento per popViewController


113

Quando si elimina un controller di visualizzazione modale utilizzando dismissViewController, è possibile fornire un blocco di completamento. Esiste un equivalente simile per popViewController?

L'argomento del completamento è abbastanza utile. Ad esempio, posso usarlo per tenere a bada la rimozione di una riga da una tableview fino a quando il modal non è fuori dallo schermo, consentendo all'utente di vedere l'animazione della riga. Quando torno da un controller di visualizzazione pushed, vorrei la stessa opportunità.

Ho provato a inserire popViewControllerun UIViewblocco di animazione, dove ho accesso a un blocco di completamento. Tuttavia, questo produce alcuni effetti collaterali indesiderati sulla vista visualizzata.

Se non è disponibile un metodo di questo tipo, quali sono le soluzioni alternative?


stackoverflow.com/a/33767837/2774520 penso che in questo modo sia il più nativo
Oleksii Nezhyborets,


3
Per il 2018 questo è molto semplice e standard: stackoverflow.com/a/43017103/294884
Fattie

Risposte:


199

So che una risposta è stata accettata più di due anni fa, tuttavia questa risposta è incompleta.

Non c'è modo di fare quello che vuoi fuori dagli schemi

Questo è tecnicamente corretto perché l' UINavigationControllerAPI non offre alcuna opzione per questo. Tuttavia, utilizzando il framework CoreAnimation è possibile aggiungere un blocco di completamento all'animazione sottostante:

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

[self.navigationController popViewControllerAnimated:YES];

[CATransaction commit];

Il blocco di completamento verrà chiamato non appena l'animazione utilizzata da popViewControllerAnimated:termina. Questa funzionalità è disponibile da iOS 4.


5
L'ho inserito in un'estensione di UINavigationController in Swift:extension UINavigationController { func popViewControllerWithHandler(handler: ()->()) { CATransaction.begin() CATransaction.setCompletionBlock(handler) self.popViewControllerAnimated(true) CATransaction.commit() } }
Arbitur

1
Non sembra funzionare per me, quando eseguo completamentoHandler su dismissViewController, la vista che lo presentava fa parte della gerarchia della vista. Quando faccio lo stesso con CATransaction, ricevo un avviso che la vista non fa parte della gerarchia della vista.
moger777

1
OK, sembra che i tuoi lavori se invertano il blocco di inizio e completamento. Mi dispiace per il voto
negativo

7
Sì, sembrava che sarebbe fantastico, ma non sembra funzionare (almeno su iOS 8). Il blocco di completamento viene chiamato immediatamente. Probabilmente a causa della combinazione di animazioni principali con animazioni in stile UIView.
bloccatoj

5
QUESTO NON FUNZIONA
durazno

51

Per la versione SWIFT di iOS9 - funziona come un fascino (non era stato testato per le versioni precedenti). Basato su questa risposta

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

Non funzionerà se non è animato, dovrebbe completare il ciclo successivo per farlo correttamente.
rshev

@rshev perché al prossimo runloop?
Ben Sinclair

@ Anddy da quello che ricordo di aver sperimentato questo, qualcosa non era stato ancora propagato a quel punto. Prova a sperimentarlo, adoro sentire come funziona per te.
rshev

@rshev Penso di averlo fatto allo stesso modo prima, devo ricontrollare. I test attuali funzionano bene.
Ben Sinclair

1
@LanceSamaria suggerisco di usare viewDidDisappear. Controlla se la barra di navigazione è disponibile, in caso contrario: non è mostrata nella barra di navigazione, quindi è stata visualizzata. if (self.navigationController == nil) {trigger your action}
HotJard

32

Ho realizzato una Swiftversione con estensioni con risposta @JorisKluivers .

Ciò richiederà una chiusura del completamento dopo che l'animazione è stata eseguita per entrambi pushe 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()
    }
}

Per me, in iOS 8.4, scritto in ObjC, il blocco si attiva a metà dell'animazione. Questo si attiva davvero al momento giusto se scritto in Swift (8.4)?
Julian F. Weinert

@ Il blocco di completamento di Arbitur viene effettivamente chiamato dopo aver chiamato popViewControllero pushViewController, ma se controlli cosa è il topViewController subito dopo, noterai che è ancora quello vecchio, proprio come popo pushnon è mai successo ...
Bogdan Razvan

@BogdanRazvan subito dopo cosa? La chiusura del completamento viene richiamata una volta completata l'animazione?
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()
}
}

17

Ho avuto lo stesso problema. E poiché ho dovuto usarlo in più occasioni e all'interno di catene di blocchi di completamento, ho creato questa soluzione generica in una sottoclasse 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];
}

assumendo

@interface NavigationController : UINavigationController <UINavigationControllerDelegate>

e

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

e

- (id) initWithRootViewController:(UIViewController *) rootViewController {
    self = [super initWithRootViewController:rootViewController];
    if (self) {
        self.delegate = self;
    }
    return self;
}

1
Mi piace molto questa soluzione, la proverò con una categoria e un oggetto associato.
spstanley

@spstanley devi pubblicare questo pod :)
k06a


15

Non c'è modo di fare quello che vuoi fuori dagli schemi. cioè non esiste un metodo con un blocco di completamento per estrarre un controller di visualizzazione da uno stack di navigazione.

Quello che farei è inserire la logica viewDidAppear. Verrà richiamato quando la visualizzazione avrà terminato di essere visualizzata sullo schermo. Verrà chiamato per tutti i diversi scenari del controller di visualizzazione che appare, ma dovrebbe andare bene.

Oppure potresti usare il UINavigationControllerDelegatemetodo navigationController:didShowViewController:animated:per fare una cosa simile. Viene chiamato quando il controller di navigazione ha terminato di premere o aprire un controller di visualizzazione.


Ho provato a farlo. Stavo memorizzando un array di "indici di riga eliminati" e ogni volta che viene visualizzata la vista, controllando se è necessario rimuovere qualcosa. È diventato rapidamente ingombrante, ma potrei dargli un'altra possibilità. Mi chiedo perché Apple lo fornisce per una transizione ma non per l'altra?
Ben Packard

1
È solo molto nuovo su dismissViewController. Forse finirà popViewController. File un radar :-).
mattjgalloway

Seriamente, però, fai un radar. È più probabile che ce la faccia se le persone lo chiedono.
mattjgalloway

1
È il posto giusto per chiederlo. C'è un'opzione per la classificazione come "Caratteristica".
mattjgalloway

3
Questa risposta non è del tutto corretta. Sebbene non sia possibile impostare il blocco di nuovo stile come attivato -dismissViewController:animated:completionBlock:, ma è possibile ottenere l'animazione tramite il delegato del controller di navigazione. Dopo che l'animazione è completa, -navigationController:didShowViewController:animated:verrà chiamato il delegato e potrai fare tutto ciò di cui hai bisogno proprio lì.
Jason Coco

13

Lavorare correttamente con o senza animazione e include anche 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)
  }
}

Qualche motivo particolare per cui chiami completion()asincrono?
leviatano

1
quando l'animazione con coordinator completionnon viene mai eseguita sullo stesso runloop. questo garantisce che completionnon venga mai eseguito sullo stesso runloop quando non si anima. è meglio non avere questo tipo di incoerenza.
rshev

11

Basato sulla risposta di @ HotJard, quando tutto ciò che desideri sono solo un paio di righe di codice. Facile e veloce.

Swift 4 :

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

6

Per il 2018 ...

se hai questo ...

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

e vuoi aggiungere un completamento ...

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

è così semplice.

Suggerimento pratico ...

È lo stesso affare per il pratico popToViewController chiamata a .

Una cosa tipica è che hai uno stack di onboarding di un milione di schermi. Quando finalmente hai finito, torni alla schermata di "base", quindi apri l'app.

Quindi nella schermata "base", per tornare indietro "fino in fondo", 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()
}

5

Il blocco di completamento viene chiamato dopo che il metodo viewDidDisappear è stato chiamato sul controller della vista presentato, quindi l'inserimento del codice nel metodo viewDidDisappear del controller della vista estratta dovrebbe funzionare come un blocco di completamento.


Certo, tranne che devi gestire tutti i casi in cui la vista sta scomparendo per qualche altro motivo.
Ben Packard

1
@ BenPackard, sì, e lo stesso vale per averlo messo in viewDidAppear nella risposta che hai accettato.
rdelmar

5

Risposta rapida 3, grazie a questa risposta: 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()
    }
}

4

Versione di Swift 4 con parametro viewController opzionale per visualizzarne uno specifico.

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

La risposta accettata sembra funzionare nel mio ambiente di sviluppo con tutti gli emulatori / dispositivi che ho, ma ricevo comunque segnalazioni di bug dagli utenti di produzione. Non sono sicuro che questo risolverà il problema di produzione, ma lasciatemi votare a favore solo in modo che qualcuno possa provarlo se ottiene lo stesso problema dalla risposta accettata.
Sean

4

Versione di Swift 4 pulita in base a questa risposta .

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


2

2020 Swift 5.1 modo

Questa soluzione garantisce che il completamento venga eseguito dopo che popViewController è completamente terminato. Puoi testarlo eseguendo un'altra operazione sul NavigationController al termine: in tutte le altre soluzioni sopra UINavigationController è ancora impegnato con l'operazione popViewController e non risponde.

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

1

Solo per completezza, ho creato una categoria Objective-C pronta per l'uso:

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

1

Ho ottenuto esattamente questo con precisione utilizzando un blocco. Volevo che il mio controller dei risultati recuperati mostrasse la riga aggiunta dalla visualizzazione modale, solo una volta che aveva lasciato completamente lo schermo, in modo che l'utente potesse vedere il cambiamento in corso. In preparazione per segue, che è responsabile della visualizzazione del controller di visualizzazione modale, ho impostato il blocco che voglio eseguire quando il modale scompare. E nel controller di visualizzazione modale sovrascrivo viewDidDissapear e quindi chiamo il blocco. Inizio semplicemente gli aggiornamenti quando il modale sta per apparire e termina gli aggiornamenti quando scompare, ma questo perché sto usando un NSFetchedResultsController, tuttavia puoi fare quello che vuoi all'interno del blocco.

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

1

Usa l'estensione successiva sul tuo codice: (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()
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.