Bloco de conclusão para popViewController


113

Ao descartar um controlador de visualização modal usando dismissViewController, há a opção de fornecer um bloco de conclusão. Existe um equivalente semelhante para popViewController?

O argumento de conclusão é bastante útil. Por exemplo, posso usá-lo para evitar a remoção de uma linha de um tableview até que o modal esteja fora da tela, permitindo que o usuário veja a animação da linha. Ao retornar de um controlador de visualização empurrado, gostaria da mesma oportunidade.

Tentei colocar popViewControllerem um UIViewbloco de animação, onde tenho acesso a um bloco de conclusão. No entanto, isso produz alguns efeitos colaterais indesejados na exibição para a qual o pop-up está sendo exibido.

Se esse método não estiver disponível, quais são algumas soluções alternativas?


stackoverflow.com/a/33767837/2774520 acho que desta forma é a mais nativa
Oleksii Nezhyborets


3
Para 2018, isso é muito simples e padrão: stackoverflow.com/a/43017103/294884
Fattie

Respostas:


199

Sei que uma resposta foi aceita há mais de dois anos, mas essa resposta está incompleta.

Não há como fazer o que você quer fora da caixa

Isso é tecnicamente correto porque a UINavigationControllerAPI não oferece opções para isso. No entanto, usando a estrutura CoreAnimation, é possível adicionar um bloco de conclusão à animação subjacente:

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

[self.navigationController popViewControllerAnimated:YES];

[CATransaction commit];

O bloco de conclusão será chamado assim que a animação usada por popViewControllerAnimated:terminar. Esta funcionalidade está disponível desde o iOS 4.


5
Coloquei isso em uma extensão do UINavigationController em Swift:extension UINavigationController { func popViewControllerWithHandler(handler: ()->()) { CATransaction.begin() CATransaction.setCompletionBlock(handler) self.popViewControllerAnimated(true) CATransaction.commit() } }
Arbitur

1
Não parece funcionar para mim, quando eu executo completeHandler em declinarViewController, a visão que estava apresentando faz parte da hierarquia de visão. Quando faço o mesmo com a CATransaction, recebo um aviso de que a visualização não faz parte da hierarquia de visualizações.
moger777

1
OK, parece que você está funcionando se você inverter o bloco de início e conclusão. Desculpe pelo voto negativo, mas o estouro de pilha não me permite mudar :(
moger777

7
Sim, parecia que seria incrível, mas não parece funcionar (pelo menos no iOS 8). O bloco de conclusão está sendo chamado imediatamente. Provavelmente devido à mistura de animações principais com animações de estilo UIView.
stickj

5
ISTO NÃO FUNCIONA
durazno,

51

Para a versão iOS9 SWIFT - funciona perfeitamente (não tinha testado para versões anteriores). Com base nesta resposta

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

Não funcionará se não for animado, deve ser concluído no próximo runloop para fazê-lo corretamente.
rshev

@rshev, por que no próximo runloop?
Ben Sinclair de

@Andy, pelo que me lembro de experimentar isso, algo ainda não havia sido propagado naquele ponto. Experimente, adoro saber como funciona para você.
rshev

@rshev Acho que já fiz a mesma coisa antes, preciso verificar novamente. Os testes atuais funcionam bem.
Ben Sinclair

1
@LanceSamaria Eu sugiro usar viewDidDisappear. Verifique se a barra de navegação está disponível, caso contrário - ela não é exibida na barra de navegação, por isso foi exibida. if (self.navigationController == nil) {acionar sua ação}
HotJard

32

Fiz uma Swiftversão com extensões com a resposta @JorisKluivers .

Isso chamará um fechamento de conclusão após a animação ser feita para 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()
    }
}

Para mim, no iOS 8.4, escrito em ObjC, o bloco dispara na metade da animação. Isso realmente dispara no momento certo se escrito em Swift (8.4)?
Julian F. Weinert

O bloco de conclusão @Arbitur é realmente chamado depois de chamar popViewControllerou pushViewController, mas se você verificar o que o topViewController é logo depois, você notará que ainda é o antigo, assim como popou pushnunca aconteceu ...
Bogdan Razvan

@BogdanRazvan logo depois o quê? O seu encerramento de conclusão é chamado quando a animação é concluída?
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

Eu tive o mesmo problema. E como tive que usá-lo em várias ocasiões e dentro de cadeias de blocos de conclusão, criei esta solução genérica em uma subclasse 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];
}

Assumindo

@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
Gosto muito dessa solução, vou tentar com uma categoria e um objeto associado.
spstanley

@spstanley, você precisa publicar este pod :)
k06a


15

Não há como fazer o que você quer fora da caixa. ou seja, não existe nenhum método com um bloco de conclusão para retirar um controlador de visualização de uma pilha de navegação.

O que eu faria é colocar a lógica viewDidAppear. Isso será chamado quando a visualização terminar de aparecer na tela. Ele será chamado para todos os diferentes cenários de exibição do controlador de visualização, mas isso deve funcionar.

Ou você pode usar o UINavigationControllerDelegatemétodo navigationController:didShowViewController:animated:para fazer algo semelhante. Isso é chamado quando o controlador de navegação termina de empurrar ou abrir um controlador de visualização.


Eu tentei isso. Eu estava armazenando uma matriz de 'índices de linha excluídos' e sempre que a visualização aparece, verificando se algo precisa ser removido. Rapidamente ficou difícil de manejar, mas posso tentar outra vez. Eu me pergunto por que a Apple oferece isso para uma transição, mas não para outra?
Ben Packard de

1
Só é muito novo no dismissViewController. Talvez aconteça popViewController. Arquive um radar :-).
mattjgalloway

Sério, registre um radar. É mais provável que consiga entrar se as pessoas pedirem.
mattjgalloway

1
Esse é o lugar certo para pedir. Existe uma opção para a classificação ser 'Característica'.
mattjgalloway

3
Esta resposta não está totalmente correta. Embora você não possa definir o bloco de novo estilo como ativado -dismissViewController:animated:completionBlock:, pode obter a animação por meio do delegado do controlador de navegação. Depois de concluída a animação, -navigationController:didShowViewController:animated:será chamado o delegado e você poderá fazer o que precisar ali mesmo.
Jason Coco,

13

Trabalhar com ou sem animação adequadamente e também inclui 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)
  }
}

Algum motivo específico para chamar o completion()assíncrono?
leviatã

1
ao animar com o coordenador completionnunca é executado no mesmo runloop. isso garante completionnunca ser executado no mesmo runloop quando não estiver em animação. é melhor não ter esse tipo de inconsistência.
rshev

11

Com base na resposta de @ HotJard, quando tudo o que você deseja são apenas algumas linhas de código. Rápido e fácil.

Swift 4 :

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

6

Para 2018 ...

se você tem isso ...

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

e você deseja adicionar uma conclusão ...

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

é simples assim.

Dica útil ...

É a mesma coisa para uma popToViewControllerchamada acessível .

Uma coisa típica é você ter uma pilha integrada de um zilhão de telas. Quando finalmente terminar, você volta à sua tela "base" e, finalmente, inicia o aplicativo.

Então, na tela "base", para ir "todo o caminho de volta", 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

O bloco de conclusão é chamado depois que o método viewDidDisappear é chamado no controlador de exibição apresentado, portanto, colocar o código no método viewDidDisappear do controlador de exibição exibido deve funcionar da mesma forma que um bloco de conclusão.


Claro - exceto que você terá que lidar com todos os casos em que a visualização está desaparecendo por algum outro motivo.
Ben Packard de

1
@BenPackard, sim, e o mesmo vale para colocá-lo em viewDidAppear na resposta que você aceitou.
rdelmar

5

Resposta do Swift 3, graças a esta resposta: 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

Versão do Swift 4 com parâmetro opcional viewController para acessar um 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()
        }
    }
}

A resposta aceita parece funcionar em meu ambiente de desenvolvimento com todos os emuladores / dispositivos que tenho, mas ainda recebo o bug relatado pelos usuários de produção. Não tenho certeza se isso resolverá o problema de produção, mas deixe-me votar a favor para que alguém possa tentar se obtiver o mesmo problema com a resposta aceita.
Sean

4

Limpei a versão do Swift 4 com base nesta resposta .

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 way

Esta solução garante que a conclusão seja executada depois que popViewController estiver totalmente concluído. Você pode testá-lo executando outra operação no NavigationController na conclusão: Em todas as outras soluções acima, o UINavigationController ainda está ocupado com a operação popViewController e não 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)
    }
}

1

Apenas para completar, preparei uma categoria Objective-C 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

1

Consegui exatamente isso com precisão usando um bloco. Eu queria que meu controlador de resultados buscados mostrasse a linha que foi adicionada pela visualização modal, apenas depois de ter saído totalmente da tela, para que o usuário pudesse ver a mudança acontecendo. Em preparação para segue, que é responsável por mostrar o controlador de visualização modal, eu defino o bloco que desejo executar quando o modal desaparecer. E no controlador de visualização modal eu substituo viewDidDissapear e chamo o bloco. Simplesmente começo as atualizações quando o modal vai aparecer e as termino quando ele desaparece, mas isso é porque estou usando um NSFetchedResultsController, entretanto, você pode fazer o que quiser dentro do bloco.

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

Use a próxima extensão em seu 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()
    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.