retorno de chamada do botão de retorno em navigationController no iOS

102

Eu empurrei uma visualização no controlador de navegação e quando pressiono o botão Voltar, ela vai para a visualização anterior automaticamente. Quero fazer algumas coisas quando o botão Voltar é pressionado antes de tirar a visualização da pilha. Qual é a função de retorno de chamada do botão Voltar?

Namratha
fonte
Verifique esta [solução] [1] que também mantém o estilo do botão Voltar. [1]: stackoverflow.com/a/29943156/3839641
Sarasranglt

Respostas:

162

A resposta de William Jockusch resolve esse problema com um truque fácil.

-(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];
}
ymutlu
fonte
32
Este código não é executado apenas quando o usuário toca no botão Voltar, mas em todos os eventos a visualização é exibida (por exemplo, ao ter um botão Concluído ou Salvar no lado direito).
assuntos de significado
7
Ou ao avançar para uma nova visão.
GuybrushThreepwood
Isso também é chamado quando o usuário faz uma panorâmica a partir da borda esquerda (interativoPopGestureRecognizer). No meu caso, estou procurando especificamente quando o usuário pressiona de volta enquanto NÃO faz o pan da borda esquerda.
Kyle Clegg
2
Não significa que o botão Voltar foi a causa. Pode ser uma transição relaxante, por exemplo.
smileBot de
1
Eu tenho uma dúvida, por que não devemos fazer isso em viewDidDisappear?
JohnVanDijk
85

Na minha opinião a melhor solução.

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

Mas só funciona com iOS5 +

Em branco
fonte
3
Esta técnica não pode distinguir entre um toque no botão Voltar e uma segue de desenrolar.
smileBot
Os métodos willMoveToParentViewController e viewWillDisappear não explicam que o controlador deve ser destruído, didMoveToParentViewController está certo
Hank,
27

provavelmente é melhor substituir o botão de fundo para que você possa manipular o evento antes que a visualização seja exibida para coisas como a confirmação do usuário.

em viewDidLoad crie um UIBarButtonItem e defina self.navigationItem.leftBarButtonItem para ele passando em um sel

- (void) viewDidLoad
{
// change the back button to cancel and add an event handler
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:@”back
style:UIBarButtonItemStyleBordered
target:self
action:@selector(handleBack:)];

self.navigationItem.leftBarButtonItem = backButton;
[backButton release];

}
- (void) handleBack:(id)sender
{
// pop to root view controller
[self.navigationController popToRootViewControllerAnimated:YES];

}

Em seguida, você pode fazer coisas como acionar um UIAlertView para confirmar a ação e abrir o controlador de visualização, etc.

Ou, em vez de criar um novo botão de fundo, você pode obedecer aos métodos de delegado UINavigationController para realizar ações quando o botão Voltar é pressionado.

Roocell
fonte
O UINavigationControllerDelegatenão possui métodos que são chamados quando o botão Voltar é pressionado.
assuntos de significado
Esta técnica permite a validação dos dados do controlador de visualização e o retorno condicional do botão Voltar do controlador de navegação.
gjpc de
Esta solução quebra o recurso de deslizar nas bordas do iOS 7
Liron Yahdav
9

Esta é a maneira correta de detectar isso.

- (void)willMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil){
        //do stuff

    }
}

esse método é chamado quando a visualização também é enviada. Portanto, verificar parent == nil é para retirar o controlador de visualização da pilha

Saad
fonte
9

Eu acabo com essas soluções. Conforme tocamos no botão Voltar, o método viewDidDisappear é chamado. podemos verificar chamando o seletor isMovingFromParentViewController que retorna verdadeiro. podemos passar os dados de volta (usando Delegate). Espero que isso ajude alguém.

-(void)viewDidDisappear:(BOOL)animated{

    if (self.isMovingToParentViewController) {

    }
    if (self.isMovingFromParentViewController) {
       //moving back
        //pass to viewCollection delegate and update UI
        [self.delegateObject passBackSavedData:self.dataModel];

    }
}
Avijit Nagare
fonte
Não se esqueça[super viewDidDisappear:animated]
SamB
9

Talvez seja um pouco tarde, mas eu também queria o mesmo comportamento antes. E a solução que escolhi funciona muito bem em um dos aplicativos atualmente na App Store. Como não vi ninguém usar um método semelhante, gostaria de compartilhá-lo aqui. A desvantagem dessa solução é que ela requer subclasses UINavigationController. Embora usar o Método Swizzling possa ajudar a evitar isso, não fui tão longe.

Portanto, o botão Voltar padrão é gerenciado por UINavigationBar. Quando um usuário toca no botão Voltar, UINavigationBarpergunte a seu delegado se ele deve abrir a tampa UINavigationItemchamando navigationBar(_:shouldPop:). UINavigationControllerrealmente implementa isso, mas não declara publicamente que adota UINavigationBarDelegate(por quê !?). Para interceptar este evento, crie uma subclasse de UINavigationController, declare sua conformidade UINavigationBarDelegatee implemente navigationBar(_:shouldPop:). Retorne truese o item de cima deve ser estourado. Retorne falsese for para ficar.

Existem dois problemas. A primeira é que você deve chamar a UINavigationControllerversão de navigationBar(_:shouldPop:)em algum momento. Mas UINavigationBarControllernão declara publicamente que está em conformidade com UINavigationBarDelegate, tentar chamá-lo resultará em um erro de tempo de compilação. A solução que escolhi é usar o tempo de execução Objective-C para obter a implementação diretamente e chamá-la. Por favor, me avise se alguém tiver uma solução melhor.

O outro problema é que navigationBar(_:shouldPop:)é chamado primeiro segue por popViewController(animated:)se o usuário tocar no botão Voltar. A ordem é invertida se o controlador de visualização for acionado chamando popViewController(animated:). Neste caso, utilizo um booleano para detectar se popViewController(animated:)é chamado antes, o navigationBar(_:shouldPop:)que significa que o usuário pressionou o botão Voltar.

Além disso, faço uma extensão de UIViewControllerpara permitir que o controlador de navegação pergunte ao controlador de visualização se ele deve ser exibido se o usuário tocar no botão Voltar. Os controladores de visualização podem retornar falsee fazer quaisquer ações necessárias e chamar popViewController(animated:)mais tarde.

class InterceptableNavigationController: UINavigationController, UINavigationBarDelegate {
    // If a view controller is popped by tapping on the back button, `navigationBar(_:, shouldPop:)` is called first follows by `popViewController(animated:)`.
    // If it is popped by calling to `popViewController(animated:)`, the order reverses and we need this flag to check that.
    private var didCallPopViewController = false

    override func popViewController(animated: Bool) -> UIViewController? {
        didCallPopViewController = true
        return super.popViewController(animated: animated)
    }

    func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        // If this is a subsequence call after `popViewController(animated:)`, we should just pop the view controller right away.
        if didCallPopViewController {
            return originalImplementationOfNavigationBar(navigationBar, shouldPop: item)
        }

        // The following code is called only when the user taps on the back button.

        guard let vc = topViewController, item == vc.navigationItem else {
            return false
        }

        if vc.shouldBePopped(self) {
            return originalImplementationOfNavigationBar(navigationBar, shouldPop: item)
        } else {
            return false
        }
    }

    func navigationBar(_ navigationBar: UINavigationBar, didPop item: UINavigationItem) {
        didCallPopViewController = false
    }

    /// Since `UINavigationController` doesn't publicly declare its conformance to `UINavigationBarDelegate`,
    /// trying to called `navigationBar(_:shouldPop:)` will result in a compile error.
    /// So, we'll have to use Objective-C runtime to directly get super's implementation of `navigationBar(_:shouldPop:)` and call it.
    private func originalImplementationOfNavigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        let sel = #selector(UINavigationBarDelegate.navigationBar(_:shouldPop:))
        let imp = class_getMethodImplementation(class_getSuperclass(InterceptableNavigationController.self), sel)
        typealias ShouldPopFunction = @convention(c) (AnyObject, Selector, UINavigationBar, UINavigationItem) -> Bool
        let shouldPop = unsafeBitCast(imp, to: ShouldPopFunction.self)
        return shouldPop(self, sel, navigationBar, item)
    }
}

extension UIViewController {
    @objc func shouldBePopped(_ navigationController: UINavigationController) -> Bool {
        return true
    }
}

E em você visualiza os controladores, implemente shouldBePopped(_:). Se você não implementar este método, o comportamento padrão será abrir o controlador de visualização assim que o usuário tocar no botão Voltar, como de costume.

class MyViewController: UIViewController {
    override func shouldBePopped(_ navigationController: UINavigationController) -> Bool {
        let alert = UIAlertController(title: "Do you want to go back?",
                                      message: "Do you really want to go back? Tap on \"Yes\" to go back. Tap on \"No\" to stay on this screen.",
                                      preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil))
        alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { _ in
            navigationController.popViewController(animated: true)
        }))
        present(alert, animated: true, completion: nil)
        return false
    }
}

Você pode ver minha demonstração aqui .

insira a descrição da imagem aqui

yusuke024
fonte
Esta é uma solução incrível e deve ser incluída em uma postagem de blog! Parece ser um exagero para o que estou procurando agora, mas em outras circunstâncias, vale a pena tentar.
ASSeeger
6

Para "ANTES de retirar a visualização da pilha":

- (void)willMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil){
        NSLog(@"do whatever you want here");
    }
}
Anum Malik
fonte
5

Existe uma maneira mais apropriada do que perguntar aos viewControllers. Você pode tornar seu controlador um delegado da barra de navegação que possui o botão Voltar. Aqui está um exemplo. Na implementação do controlador em que você deseja lidar com o pressionamento do botão Voltar, diga que ele implementará o protocolo UINavigationBarDelegate:

@interface MyViewController () <UINavigationBarDelegate>

Então, em algum lugar em seu código de inicialização (provavelmente em viewDidLoad) torne seu controlador o delegado de sua barra de navegação:

self.navigationController.navigationBar.delegate = self;

Finalmente, implemente o método shouldPopItem. Este método é chamado logo que o botão Voltar é pressionado. Se você tiver vários controladores ou itens de navegação na pilha, provavelmente desejará verificar quais desses itens de navegação estão sendo exibidos (o parâmetro do item), de modo que você só faça suas coisas personalizadas quando espera. Aqui está um exemplo:

-(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    NSLog(@"Back button got pressed!");
    //if you return NO, the back button press is cancelled
    return YES;
}
Carlos Guzman
fonte
4
não funcionou para mim .. lamentável porque é magro. "*** Encerrando aplicativo devido à exceção não detectada 'NSInternalInconsistencyException', motivo: 'Não é possível definir manualmente o delegado em um UINavigationBar gerenciado por um controlador.'"
DynamicDan
Infelizmente, isso não funcionará com um UINavigationController; em vez disso, você precisa de um UIViewController padrão com um UINavigationBar nele. Isso significa que você não pode tirar proveito de vários push e popping do viewcontroller automático que o NavigationController oferece. Desculpe!
Carlos Guzman
Acabei de usar o UINavigationBar em vez do NavigationBarController e ele funciona bem. Eu sei que a pergunta é sobre o NavigationBarController, mas essa solução é enxuta.
appsunited
3

Se você não puder usar "viewWillDisappear" ou método semelhante, tente criar uma subclasse de UINavigationController. Esta é a classe de cabeçalho:

#import <Foundation/Foundation.h>
@class MyViewController;

@interface CCNavigationController : UINavigationController

@property (nonatomic, strong) MyViewController *viewController;

@end

Classe de implementação:

#import "CCNavigationController.h"
#import "MyViewController.h"

@implementation CCNavigationController {

}
- (UIViewController *)popViewControllerAnimated:(BOOL)animated {
    @"This is the moment for you to do whatever you want"
    [self.viewController doCustomMethod];
    return [super popViewControllerAnimated:animated];
}

@end

Por outro lado, você precisa vincular este viewController ao seu NavigationController personalizado, portanto, em seu método viewDidLoad para seu viewController regular, faça o seguinte:

@implementation MyViewController {
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        ((CCNavigationController*)self.navigationController).viewController = self;
    }
}
George Harley
fonte
3

Aqui está outra maneira que implementei (não testei com um segue de desenrolamento, mas provavelmente não iria diferenciar, como outros afirmaram em relação a outras soluções nesta página) para fazer com que o controlador de visualização pai execute ações antes do VC filho que ele empurrou é retirado da pilha de visualização (eu usei alguns níveis abaixo do UINavigationController original). Isso também pode ser usado para executar ações antes que o childVC seja enviado. Isso tem a vantagem de trabalhar com o botão Voltar do sistema iOS, em vez de criar um UIBarButtonItem ou UIButton personalizado.

  1. Peça ao seu pai VC para adotar o UINavigationControllerDelegateprotocolo e se registrar para mensagens delegadas:

    MyParentViewController : UIViewController <UINavigationControllerDelegate>
    
    -(void)viewDidLoad {
        self.navigationcontroller.delegate = self;
    }
  2. Implemente este UINavigationControllerDelegatemétodo de instância em MyParentViewController:

    - (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
        // Test if operation is a pop; can also test for a push (i.e., do something before the ChildVC is pushed
        if (operation == UINavigationControllerOperationPop) {
            // Make sure it's the child class you're looking for
            if ([fromVC isKindOfClass:[ChildViewController class]]) {
                // Can handle logic here or send to another method; can also access all properties of child VC at this time
                return [self didPressBackButtonOnChildViewControllerVC:fromVC];
            }
        }
        // If you don't want to specify a nav controller transition
        return nil;
    }
  3. Se você especificar uma função de retorno de chamada específica no UINavigationControllerDelegatemétodo de instância acima

    -(id <UIViewControllerAnimatedTransitioning>)didPressBackButtonOnAddSearchRegionsVC:(UIViewController *)fromVC {
        ChildViewController *childVC = ChildViewController.new;
        childVC = (ChildViewController *)fromVC;
    
        // childVC.propertiesIWantToAccess go here
    
        // If you don't want to specify a nav controller transition
        return nil;

    }

Evan R
fonte
1

Isso é o que funciona para mim no Swift:

override func viewWillDisappear(_ animated: Bool) {
    if self.navigationController?.viewControllers.index(of: self) == nil {
        // back button pressed or back gesture performed
    }

    super.viewWillDisappear(animated)
}
pableiros
fonte
0

Se você estiver usando um Storyboard e estiver vindo de um push segue, você também pode simplesmente substituir shouldPerformSegueWithIdentifier:sender:.

Mojo66
fonte