Detectando quando o botão 'voltar' é pressionado em uma barra de navegação

135

Preciso executar algumas ações quando o botão voltar (retornar à tela anterior, retornar à visualização dos pais) é pressionado em uma barra de navegação.

Existe algum método que eu possa implementar para capturar o evento e acionar algumas ações para pausar e salvar dados antes que a tela desapareça?

ewok
fonte
1
Olhe para a solução neste tópico
Jiri Volejnik
Eu fiz isso dessa forma, mostre a decisão aqui
Taras

Respostas:

316

ATUALIZAÇÃO: De acordo com alguns comentários, a solução na resposta original parece não funcionar em certos cenários no iOS 8+. Não posso verificar se esse é realmente o caso sem mais detalhes.

Para aqueles de vocês, porém, nessa situação, há uma alternativa. Detectar quando um controlador de exibição está sendo acionado é possível substituindo-o willMove(toParentViewController:). A idéia básica é que um controlador de exibição esteja sendo acionado quando parentestiver nil.

Confira "Implementando um Container View Controller" para obter mais detalhes.


Desde o iOS 5, descobri que a maneira mais fácil de lidar com essa situação é usar o novo método - (BOOL)isMovingFromParentViewController:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isMovingFromParentViewController) {
    // Do your stuff here
  }
}

- (BOOL)isMovingFromParentViewController faz sentido quando você está pressionando e acionando controladores em uma pilha de navegação.

No entanto, se você estiver apresentando controladores de exibição modal, use - (BOOL)isBeingDismissed:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isBeingDismissed) {
    // Do your stuff here
  }
}

Conforme observado nesta pergunta , você pode combinar as duas propriedades:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isMovingFromParentViewController || self.isBeingDismissed) {
    // Do your stuff here
  }
}

Outras soluções dependem da existência de a UINavigationBar. Em vez disso, goste mais da minha abordagem, porque desacopla as tarefas necessárias a serem executadas da ação que acionou o evento, ou seja, pressionar o botão Voltar.

elitalon
fonte
Eu gosto da sua resposta. Mas por que você usou 'self.isBeingDismissed'? No meu caso, as declarações em 'self.isBeingDismissed' não são implementadas.
Rutvij Kotecha
3
self.isMovingFromParentViewControllertem valor TRUE quando estou exibindo a pilha de navegação programaticamente usando popToRootViewControllerAnimated- sem nenhum toque no botão voltar. Devo reduzir a sua resposta? (o sujeito diz "botão 'voltar' é pressionado em uma barra de navegação")
kas-kad
2
Ótima resposta, muito obrigado. Em Swift eu usei:override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) if isMovingFromParentViewController(){ println("back button pressed") } }
Camillo Visini
1
Você só deve fazer isso dentro de -viewDidDisappear:uma vez que é possível que você vai ter um -viewWillDisappear:sem -viewDidDisappear:(como quando você começar passando para descartar um item de controlador de navegação e, em seguida, cancelar o furto.
Heath Borders
3
Parece que não é mais uma solução confiável. Trabalhei no momento em que o usei pela primeira vez (era o iOS 10). Mas agora descobri que acidentalmente parou de funcionar (iOS 11). Teve que mudar para a solução "willMove (toParentViewController)".
Vitalii
100

Enquanto viewWillAppear()e viewDidDisappear() são chamados quando o botão Voltar é pressionado, eles também são chamados em outros momentos. Veja o final da resposta para mais informações.

Usando UIViewController.parent

A detecção do botão Voltar é melhor quando o VC é removido do pai (o NavigationController) com a ajuda de willMoveToParentViewController(_:)OUdidMoveToParentViewController()

Se o pai for nulo, o controlador de exibição está sendo retirado da pilha de navegação e descartado. Se o pai não for nulo, ele será adicionado à pilha e apresentado.

// Objective-C
-(void)willMoveToParentViewController:(UIViewController *)parent {
     [super willMoveToParentViewController:parent];
    if (!parent){
       // The back button was pressed or interactive gesture used
    }
}


// Swift
override func willMove(toParent parent: UIViewController?) {
    super.willMove(toParent: parent)
    if parent == nil {
        // The back button was pressed or interactive gesture used
    }
}

Trocar willMovepara didMovee verificação self.parent para fazer o trabalho após o controlador de vista é indeferido.

Parando a dispensa

Observe que verificar o pai não permite "pausar" a transição se você precisar fazer algum tipo de salvamento assíncrono. Para fazer isso, você pode implementar o seguinte. A única desvantagem aqui é que você perde o elegante botão de estilo / animação do iOS. Também tenha cuidado aqui com o gesto de furto interativo. Use o seguinte para lidar com este caso.

var backButton : UIBarButtonItem!

override func viewDidLoad() {
    super.viewDidLoad()

     // Disable the swipe to make sure you get your chance to save
     self.navigationController?.interactivePopGestureRecognizer.enabled = false

     // Replace the default back button
    self.navigationItem.setHidesBackButton(true, animated: false)
    self.backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Plain, target: self, action: "goBack")
    self.navigationItem.leftBarButtonItem = backButton
}

// Then handle the button selection
func goBack() {
    // Here we just remove the back button, you could also disabled it or better yet show an activityIndicator
    self.navigationItem.leftBarButtonItem = nil
    someData.saveInBackground { (success, error) -> Void in
        if success {
            self.navigationController?.popViewControllerAnimated(true)
            // Don't forget to re-enable the interactive gesture
            self.navigationController?.interactivePopGestureRecognizer.enabled = true
        }
        else {
            self.navigationItem.leftBarButtonItem = self.backButton
            // Handle the error
        }
    }
}


Mais informações aparecerão / apareceram

Se você não conseguiu o viewWillAppear viewDidDisappearproblema, vamos dar um exemplo. Digamos que você tenha três controladores de exibição:

  1. ListVC: Uma exibição de tabela das coisas
  2. DetailVC: detalhes sobre uma coisa
  3. SettingsVC: algumas opções para uma coisa

Vamos seguir os exorta a detailVCque você vá do listVCpara settingsVCe de volta paralistVC

Lista> Detalhe (push detailVC) Detail.viewDidAppear<- aparece
Detail> Settings (push settingsVC) Detail.viewDidDisappear<- desaparece

E à medida que voltamos ...
Configurações> Detalhe (pop settingsVC) Detail.viewDidAppear<- aparece
Detalhes> List (pop detailVC) Detail.viewDidDisappear<- desaparece

Observe que isso viewDidDisappearé chamado várias vezes, não apenas ao voltar, mas também ao avançar. Para uma operação rápida que pode ser desejada, mas para uma operação mais complexa como uma chamada de rede para salvar, talvez não.

WCByrne
fonte
Apenas uma observação, o usuário didMoveToParantViewController:deve fazer o trabalho quando a visualização não estiver mais visível. Útil para iOS7 com o InteractiveGesutre
WCByrne
didMoveToParentViewController * há um erro de digitação
thewormsterror
Não se esqueça de ligar para [super willMoveToParentViewController: parent]!
ScottyB
2
O parâmetro pai é nulo quando você está aparecendo no controlador de visualização pai e não é nulo quando a visualização em que esse método aparece está sendo mostrada. Você pode usar esse fato para executar uma ação somente quando o botão Voltar for pressionado, e não ao chegar à exibição. Afinal, essa era a pergunta original. :)
Mike
1
Isso também é chamado ao usar programaticamente _ = self.navigationController?.popViewController(animated: true), portanto, não é apenas chamado ao pressionar o botão Voltar. Estou procurando uma chamada que funcione apenas quando Voltar for pressionado.
Ethan Allen
16

Primeiro método

- (void)didMoveToParentViewController:(UIViewController *)parent
{
    if (![parent isEqual:self.parentViewController]) {
         NSLog(@"Back pressed");
    }
}

Segundo método

-(void) viewWillDisappear:(BOOL)animated {
    if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) {
       // back button was pressed.  We know this is true because self is no longer
       // in the navigation stack.  
    }
    [super viewWillDisappear:animated];
}
Zar E Ahmer
fonte
1
O segundo método foi o único que funcionou para mim. O primeiro método também foi chamado quando minha visão foi apresentada, o que não era aceitável para o meu caso de uso.
Marcshilling 23/03
10

Aqueles que afirmam que isso não funciona estão enganados:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if self.isMovingFromParent {
        print("we are being popped")
    }
}

Isso funciona bem. Então, o que está causando o mito generalizado de que não?

O problema parece ser devido a uma implementação incorreta de um método diferente , a saber, que a implementação do willMove(toParent:)esqueceu de chamar super.

Se você implementar willMove(toParent:)sem chamar super, self.isMovingFromParentserá falsee o uso de viewWillDisappearparecerá falhar. Não falhou; você o quebrou.

NOTA: O problema real é geralmente o segundo controlador de visualização que detecta que o primeiro controlador de visualização foi acionado. Veja também a discussão mais geral aqui: A detecção unificada do UIViewController "tornou-se a mais avançada"?

EDIT Um comentário sugere que isso deveria ser viewDidDisappearmelhor que viewWillDisappear.

mate
fonte
Este código é executado quando o botão Voltar é pressionado, mas também é executado se o VC for acionado programaticamente.
Biomiker
@biomiker Claro, mas isso também se aplica a outras abordagens. Estalo está estourando. A questão é como detectar um pop quando você não pop programaticamente. Se você popular programaticamente, já sabe que está populando, então não há nada para detectar.
Matt
Sim, isso é verdade para várias outras abordagens e muitas delas têm comentários semelhantes. Eu estava apenas esclarecendo, já que essa era uma resposta recente com uma refutação específica e eu tinha minhas esperanças quando a li. Para o registro, porém, a questão é como detectar o pressionar do botão Voltar. É um argumento razoável dizer que o código que também será executado em situações em que o botão Voltar não é pressionado, sem indicar se o botão Voltar foi ou não pressionado, não resolve completamente a questão real, mesmo que talvez a pergunta possa ter sido mais explícito nesse ponto.
Biomiker
1
Infelizmente, isso retorna truepara o gesto de deslize interativo - a partir da borda esquerda do controlador de exibição - mesmo que o deslize não tenha sido totalmente deslocado. Então, em vez de fazer check-in willDisappear, fazê-lo em didDisappearobras.
badhanganesh
1
@badhanganesh Obrigado, resposta editada para incluir essa informação.
matt
9

Estou brincando (ou lutando) com esse problema há dois dias. Na IMO, a melhor abordagem é apenas criar uma classe de extensão e um protocolo, como este:

@protocol UINavigationControllerBackButtonDelegate <NSObject>
/**
 * Indicates that the back button was pressed.
 * If this message is implemented the pop logic must be manually handled.
 */
- (void)backButtonPressed;
@end

@interface UINavigationController(BackButtonHandler)
@end

@implementation UINavigationController(BackButtonHandler)
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    UIViewController *topViewController = self.topViewController;
    BOOL wasBackButtonClicked = topViewController.navigationItem == item;
    SEL backButtonPressedSel = @selector(backButtonPressed);
    if (wasBackButtonClicked && [topViewController respondsToSelector:backButtonPressedSel]) {
        [topViewController performSelector:backButtonPressedSel];
        return NO;
    }
    else {
        [self popViewControllerAnimated:YES];
        return YES;
    }
}
@end

Isso funciona porque UINavigationControllerreceberá uma chamada navigationBar:shouldPopItem:sempre que um controlador de exibição for acionado. Detectamos se a tecla Voltar foi pressionada ou não (qualquer outro botão). A única coisa que você precisa fazer é implementar o protocolo no controlador de exibição em que a tecla Voltar é pressionada.

Lembre-se de colocar manualmente o controlador de exibição dentro backButtonPressedSel, se estiver tudo bem.

Se você já tiver subclassificado UINavigationViewControllere implementado navigationBar:shouldPopItem:, não se preocupe, isso não interferirá.

Você também pode estar interessado em desativar o gesto de volta.

if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
    self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
7ynk3r
fonte
1
Essa resposta foi quase completa para mim, exceto pelo fato de eu descobrir que dois controladores de visualização costumavam aparecer. Retornar YES faz com que o método de chamada chame pop, portanto, chamar pop também significava que dois controladores de exibição seriam acionados. Veja esta resposta em outra pergunta para mais deets (uma resposta muito boa que merece mais upvotes): stackoverflow.com/a/26084150/978083
Jason Ridge
Bom ponto, minha descrição não foi clara sobre esse fato. O "Lembre-se de abrir manualmente o controlador de exibição se tudo estiver ok" é apenas para o caso de retornar "NÃO", caso contrário, o fluxo é o pop normal.
7ynk3r
1
Para o ramo "else ', é melhor chamar super implementação se você não quiser lidar com o pop e deixá-lo retornar o que achar correto, o que é principalmente SIM, mas ele também cuida do pop e anima a chevron adequadamente .
Ben Sinclair
9

Isso funciona para mim no iOS 9.3.x com Swift:

override func didMoveToParentViewController(parent: UIViewController?) {
    super.didMoveToParentViewController(parent)

    if parent == self.navigationController?.parentViewController {
        print("Back tapped")
    }
}

Ao contrário de outras soluções aqui, isso não parece desencadear inesperadamente.

Chris Villa
fonte
é melhor usar WillMove vez
Eugene Gordin
4

Para o registro, acho que isso é mais do que ele estava procurando ...

    UIBarButtonItem *l_backButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRewind target:self action:@selector(backToRootView:)];

    self.navigationItem.leftBarButtonItem = l_backButton;


    - (void) backToRootView:(id)sender {

        // Perform some custom code

        [self.navigationController popToRootViewControllerAnimated:YES];
    }
Paul Brady
fonte
1
Obrigado Paul, esta solução é bastante simples. Infelizmente, o ícone é diferente. Este é o ícone "rebobinar", não o ícone voltar. Talvez haja uma maneira de usar o ícone de volta ...
Ferran Maylinch
2

Como purrrminatordiz, a resposta por elitalonnão é totalmente correta, pois your stuffseria executada mesmo quando o controlador fosse acionado programaticamente.

A solução que encontrei até agora não é muito boa, mas funciona para mim. Além do que foi elitalondito, também verifico se estou aparecendo programaticamente ou não:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if ((self.isMovingFromParentViewController || self.isBeingDismissed)
      && !self.isPoppingProgrammatically) {
    // Do your stuff here
  }
}

Você precisa adicionar essa propriedade ao seu controlador e configurá-la como YES antes de aparecer programaticamente:

self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];

Obrigado pela ajuda!

Ferran Maylinch
fonte
2

A melhor maneira é usar os métodos de delegação UINavigationController

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated

Usando isso, você pode saber qual controlador está mostrando o UINavigationController.

if ([viewController isKindOfClass:[HomeController class]]) {
    NSLog(@"Show home controller");
}
Harald
fonte
Isso deve ser marcado como a resposta correta! Também pode querer adicionar mais uma linha apenas para lembrar as pessoas -> self.navigationController.delegate = self;
Mike Critchley
2

Resolvi esse problema adicionando um UIControl à barra de navegação no lado esquerdo.

UIControl *leftBarItemControl = [[UIControl alloc] initWithFrame:CGRectMake(0, 0, 90, 44)];
[leftBarItemControl addTarget:self action:@selector(onLeftItemClick:) forControlEvents:UIControlEventTouchUpInside];
self.leftItemControl = leftBarItemControl;
[self.navigationController.navigationBar addSubview:leftBarItemControl];
[self.navigationController.navigationBar bringSubviewToFront:leftBarItemControl];

E você precisa se lembrar de removê-lo quando o modo de exibição desaparecer:

- (void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.leftItemControl) {
        [self.leftItemControl removeFromSuperview];
    }    
}

Isso é tudo!

Eric
fonte
2

Você pode usar o retorno de chamada do botão voltar, assim:

- (BOOL) navigationShouldPopOnBackButton
{
    [self backAction];
    return NO;
}

- (void) backAction {
    // your code goes here
    // show confirmation alert, for example
    // ...
}

para a versão rápida, você pode fazer algo como no escopo global

extension UIViewController {
     @objc func navigationShouldPopOnBackButton() -> Bool {
     return true
    }
}

extension UINavigationController: UINavigationBarDelegate {
     public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
          return self.topViewController?.navigationShouldPopOnBackButton() ?? true
    }
}

Abaixo, você coloca no viewcontroller onde deseja controlar a ação do botão Voltar:

override func navigationShouldPopOnBackButton() -> Bool {
    self.backAction()//Your action you want to perform.

    return true
}
Pedro Magalhães
fonte
1
Não sei por que alguém votou negativamente. Esta parece ser de longe a melhor resposta.
Avinash
@Avinash De onde navigationShouldPopOnBackButtonvem? Não faz parte da API pública.
Elitalon 29/05/19
@elitalon Desculpe, isso foi meia resposta. Eu pensei que o contexto restante estivesse lá em questão. De qualquer forma ter actualizado a resposta agora
Avinash
1

Como disse Coli88, você deve verificar o protocolo UINavigationBarDelegate.

De uma maneira mais geral, você também pode usar o - (void)viewWillDisapear:(BOOL)animatedpara executar um trabalho personalizado quando a visualização mantida pelo controlador de visualização atualmente visível estiver prestes a desaparecer. Infelizmente, isso abrangeria os casos push e pop.

Ramdam
fonte
1

Para Swift com um UINavigationController:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}
Murray Sagal
fonte
1

A resposta do 7ynk3r foi muito próxima do que eu usei no final, mas precisava de alguns ajustes:

- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {

    UIViewController *topViewController = self.topViewController;
    BOOL wasBackButtonClicked = topViewController.navigationItem == item;

    if (wasBackButtonClicked) {
        if ([topViewController respondsToSelector:@selector(navBackButtonPressed)]) {
            // if user did press back on the view controller where you handle the navBackButtonPressed
            [topViewController performSelector:@selector(navBackButtonPressed)];
            return NO;
        } else {
            // if user did press back but you are not on the view controller that can handle the navBackButtonPressed
            [self popViewControllerAnimated:YES];
            return YES;
        }
    } else {
        // when you call popViewController programmatically you do not want to pop it twice
        return YES;
    }
}
micromanc3r
fonte
0

self.navigationController.isMovingFromParentViewController não está mais funcionando no iOS8 e 9 eu uso:

-(void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.navigationController.topViewController != self)
    {
        // Is Popping
    }
}
Vassily
fonte
-1

(RÁPIDO)

solução finalmente encontrada. O método que procurávamos é "willShowViewController", que é o método delegado de UINavigationController

//IMPORT UINavigationControllerDelegate !!
class PushedController: UIViewController, UINavigationControllerDelegate {

    override func viewDidLoad() {
        //set delegate to current class (self)
        navigationController?.delegate = self
    }

    func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
        //MyViewController shoud be the name of your parent Class
        if var myViewController = viewController as? MyViewController {
            //YOUR STUFF
        }
    }
}
Jiří Zahálka
fonte
1
O problema com esta abordagem é que os casais MyViewControllerpara PushedController.
Clozach #