Definir ação para o botão Voltar no controlador de navegação

180

Estou tentando substituir a ação padrão do botão Voltar em um controlador de navegação. Forneci ao alvo uma ação no botão personalizado. O estranho é que, ao atribuí-lo, o atributo backbutton não presta atenção a eles e apenas exibe a visualização atual e volta à raiz:

UIBarButtonItem *backButton = [[UIBarButtonItem alloc] 
                                  initWithTitle: @"Servers" 
                                  style:UIBarButtonItemStylePlain 
                                  target:self 
                                  action:@selector(home)];
self.navigationItem.backBarButtonItem = backButton;

Assim que eu defini-lo através do leftBarButtonItemsobre o navigationItemque chama de minha ação, no entanto, em seguida, o botão se parece com uma simples rodada em vez do arrowed volta um:

self.navigationItem.leftBarButtonItem = backButton;

Como posso chamar a minha ação personalizada antes de voltar à visualização raiz? Existe uma maneira de substituir a ação de retorno padrão ou existe um método que sempre é chamado ao sair de uma exibição ( viewDidUnloadnão faz isso)?

Papagaios
fonte
ação: @selector (home)]; precisa de: após a ação do seletor: @selector (home :)]; caso contrário não vai funcionar
PartySoft
7
@PartySoft Isso não é verdade, a menos que o método seja declarado com dois pontos. É perfeitamente válido ter botões que selecionam seletores que não aceitam nenhum parâmetro.
mbm29414
3
Por que a Apple não fornece um botão com estilo em forma de botão Voltar? Parece bastante óbvio.
JohnK 28/06
Olhe para a solução neste tópico
Jiri Volejnik 19/18

Respostas:

363

Tente colocar isso no controlador de exibição em que deseja detectar a impressora:

-(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];
}
William Jockusch
fonte
1
esta é uma solução lisa, limpa, agradável e muito bem pensada #
boliva
6
+1 grande corte, mas não oferece controle sobre a animação de pop
matm
3
Não funciona para mim se eu enviar uma mensagem ao delegado através de um botão e o delegado exibir o controlador - isso ainda é acionado.
SAHM
21
Outro problema é que você não pode diferenciar se o usuário pressionou o botão voltar ou se você programaticamente chamou [self.navigationController popViewControllerAnimated: YES]
Chase Roberts
10
Apenas uma versão do FYI: Swift:if (find(self.navigationController!.viewControllers as! [UIViewController],self)==nil)
hEADcRASH 25/03
177

Eu implementei a extensão UIViewController-BackButtonHandler . Ele não precisa subclassificar nada, basta colocá-lo em seu projeto e substituir o navigationShouldPopOnBackButtonmétodo na UIViewControllerclasse:

-(BOOL) navigationShouldPopOnBackButton {
    if(needsShowConfirmation) {
        // Show confirmation alert
        // ...
        return NO; // Ignore 'Back' button this time
    }
    return YES; // Process 'Back' button click and Pop view controler
}

Baixe o aplicativo de exemplo .

onegray
fonte
16
Esta é a solução mais limpa que eu já vi, melhor e mais simples do que usar seu próprio UIButton personalizado. Obrigado!
ramirogm
4
O navigationBar:shouldPopItem:método não é privado, pois faz parte do UINavigationBarDelegateprotocolo.
onegray
1
mas o UINavigationController já implementa o delegado (para retornar YES)? ou será no futuro? subclassificação é provavelmente uma opção mais segura
Sam
8
Eu apenas implementei isso (BTW bem legal) no iOS 7.1 e notei que, depois de retornar, NOo botão voltar permanece em um estado desativado (visualmente, porque ele ainda recebe e reage a eventos de toque). Eu trabalhei em torno disso adicionando uma elsedeclaração à shouldPopverificação e percorrendo as subvisões da barra de navegação e configurando o alphavalor de volta para 1, se necessário, dentro de um bloco de animação: gist.github.com/idevsoftware/9754057
boliva
2
Esta é uma das melhores extensões que eu já vi. Muito obrigado.
Srikanth
42

Ao contrário de Amagrammer disse, é possível. Você precisa subclassificar o seu navigationController. Expliquei tudo aqui (incluindo código de exemplo).

HansPinckaers
fonte
A documentação da Apple ( developer.apple.com/iphone/library/documentation/UIKit/… ) diz que "Esta classe não se destina à subclasse". Embora eu não tenha certeza do que eles querem dizer com isso - eles podem significar "você normalmente não precisa fazer isso" ou eles podem significar "rejeitaremos seu aplicativo se você mexer com nosso controlador" ...
Kuba Suder
Esta é certamente a única maneira de fazê-lo. Gostaria de poder lhe dar mais pontos Hans!
precisa
1
Você pode realmente impedir que uma exibição saia usando esse método? O que você faria com que o método popViewControllerAnimated retornasse se quisesse que a exibição não saísse?
JosephH 26/08/10
1
Sim você pode. Apenas não chame o método de superclasse em sua implementação, esteja ciente! Você não deve fazer isso, o usuário espera voltar na navegação. O que você pode fazer é pedir uma confirmação. De acordo com a documentação da Apple, o popViewController retorna: "O controlador de exibição que foi retirado da pilha". Portanto, quando nada for exibido, você deve retornar nulo;
precisa saber é o seguinte
1
@HansPickaers Acho que sua resposta sobre impedir a saída de uma exibição pode estar um pouco incorreta. Se eu exibir uma mensagem de 'confirmação' da implementação das subclasses de popViewControllerAnimated:, a NavigationBar ainda anima um nível acima na árvore, independentemente do que eu retorne. Parece que isso ocorre porque, ao clicar no botão voltar, chama shouldPopNavigationItem na barra de navegação. Estou retornando nada do meu método de subclasses, conforme recomendado.
precisa saber é o seguinte
15

Versão rápida:

(de https://stackoverflow.com/a/19132881/826435 )

No seu controlador de exibição, você apenas se conforma a um protocolo e executa a ação que precisar:

extension MyViewController: NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress() -> Bool {
        performSomeActionOnThePressOfABackButton()
        return false
    }
}

Em seguida, crie uma classe, digamos NavigationController+BackButton, e copie e cole o código abaixo:

protocol NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress() -> Bool
}

extension UINavigationController {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        // Prevents from a synchronization issue of popping too many navigation items
        // and not enough view controllers or viceversa from unusual tapping
        if viewControllers.count < navigationBar.items!.count {
            return true
        }

        // Check if we have a view controller that wants to respond to being popped
        var shouldPop = true
        if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
            shouldPop = viewController.shouldPopOnBackButtonPress()
        }

        if (shouldPop) {
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        } else {
            // Prevent the back button from staying in an disabled state
            for view in navigationBar.subviews {
                if view.alpha < 1.0 {
                    UIView.animate(withDuration: 0.25, animations: {
                        view.alpha = 1.0
                    })
                }
            }

        }

        return false
    }
}
kgaidis
fonte
Talvez eu tenha perdido algo, mas não funciona para mim, o método performSomeActionOnThePressOfABackButton da extensão nunca é chamado #
Turvy
@FlorentBreton talvez um mal-entendido? shouldPopOnBackButtonPressdeve ser chamado desde que não haja bugs. performSomeActionOnThePressOfABackButtoné apenas um método inventado que não existe.
kgaidis
Eu entendi, é por isso que eu criei um método performSomeActionOnThePressOfABackButtonno meu controlador para executar uma ação específica quando o botão Voltar é pressionado, mas esse método nunca foi chamado, a ação é um retorno de volta normais
Turvy
1
Também não está trabalhando para mim. O método shouldPop nunca é chamado. Você colocou um delegado em algum lugar?
precisa saber é o seguinte
@ TimAutin Acabei de testar isso novamente e parece que algo mudou. Parte fundamental para entender que em a UINavigationController, o navigationBar.delegateé definido como o controlador de navegação. Portanto, os métodos devem ser chamados. No entanto, em Swift, não consigo que eles sejam chamados, mesmo em uma subclasse. No entanto, consegui que eles fossem chamados no Objective-C, então eu usaria a versão do Objective-C por enquanto. Pode ser um bug rápido.
precisa saber é
5

Não é possível fazer diretamente. Existem algumas alternativas:

  1. Crie seu próprio costume UIBarButtonItemque valida ao tocar e aparecer se o teste for aprovado
  2. Valide o conteúdo do campo de formulário usando um UITextFieldmétodo delegado, como -textFieldShouldReturn:, que é chamado depois que o botão Returnou Doneé pressionado no teclado

A desvantagem da primeira opção é que o estilo da seta para a esquerda do botão Voltar não pode ser acessado a partir de um botão da barra personalizada. Então você tem que usar uma imagem ou usar um botão de estilo regular.

A segunda opção é boa porque você recebe o campo de texto de volta no método delegado, para que você possa direcionar sua lógica de validação para o campo de texto específico enviado ao método de retorno de chamada delegado.

Alex Reynolds
fonte
5

Por algumas razões de segmentação, a solução mencionada por @HansPinckaers não era certa para mim, mas achei uma maneira muito mais fácil de tocar no botão Voltar, e quero colocar isso aqui, caso isso possa evitar horas de decepções para alguém. O truque é realmente fácil: basta adicionar um UIButton transparente como uma subview à sua UINavigationBar e definir seus seletores para ele como se fosse o botão real! Aqui está um exemplo usando o Monotouch e C #, mas a tradução para o objetivo-c não deve ser muito difícil de encontrar.

public class Test : UIViewController {
    public override void ViewDidLoad() {
        UIButton b = new UIButton(new RectangleF(0, 0, 60, 44)); //width must be adapted to label contained in button
        b.BackgroundColor = UIColor.Clear; //making the background invisible
        b.Title = string.Empty; // and no need to write anything
        b.TouchDown += delegate {
            Console.WriteLine("caught!");
            if (true) // check what you want here
                NavigationController.PopViewControllerAnimated(true); // and then we pop if we want
        };
        NavigationController.NavigationBar.AddSubview(button); // insert the button to the nav bar
    }
}

Curiosidade: para fins de teste e para encontrar boas dimensões para o meu botão falso, defino a cor de fundo como azul ... E aparece atrás do botão voltar! De qualquer forma, ele ainda captura qualquer toque direcionado ao botão original.

psicopata
fonte
3

Essa técnica permite alterar o texto do botão "voltar" sem afetar o título de qualquer um dos controladores de exibição ou ver o texto do botão voltar mudar durante a animação.

Adicione isso ao método init no controlador de exibição de chamada :

UIBarButtonItem *temporaryBarButtonItem = [[UIBarButtonItem alloc] init];   
temporaryBarButtonItem.title = @"Back";
self.navigationItem.backBarButtonItem = temporaryBarButtonItem;
[temporaryBarButtonItem release];
Jason Moore
fonte
3

Caminho mais fácil

Você pode usar os métodos de delegação do UINavigationController. O método willShowViewControlleré chamado quando o botão voltar do seu VC é pressionado. Faça o que quiser quando voltar btn pressionado

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
Zar E Ahmer
fonte
Verifique se o seu controlador de exibição define-se como o delegado da navigationController e conforma herdada para o protocolo UINavigationControllerDelegate
Justin Milo
3

Aqui está a minha solução Swift. Na sua subclasse de UIViewController, substitua o método navigationShouldPopOnBackButton.

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

extension UINavigationController {

    func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {
        if let vc = self.topViewController {
            if vc.navigationShouldPopOnBackButton() {
                self.popViewControllerAnimated(true)
            } else {
                for it in navigationBar.subviews {
                    let view = it as! UIView
                    if view.alpha < 1.0 {
                        [UIView .animateWithDuration(0.25, animations: { () -> Void in
                            view.alpha = 1.0
                        })]
                    }
                }
                return false
            }
        }
        return true
    }

}
AutomatonTec
fonte
A substituição do método navigationShouldPopOnBackButton no UIViewController não funciona - o programa executa o método pai, não o substituído. Alguma solução para isso? Alguém tem o mesmo problema?
Pawel Cala
ele vai todo o caminho de volta para rootview se return true
Pawriwes
@Pawriwes Aqui está uma solução que eu escrevi que parece funcionar para mim: stackoverflow.com/a/34343418/826435
kgaidis
3

Encontrei uma solução que também mantém o estilo do botão Voltar. Adicione o seguinte método ao seu controlador de exibição.

-(void) overrideBack{

    UIButton *transparentButton = [[UIButton alloc] init];
    [transparentButton setFrame:CGRectMake(0,0, 50, 40)];
    [transparentButton setBackgroundColor:[UIColor clearColor]];
    [transparentButton addTarget:self action:@selector(backAction:) forControlEvents:UIControlEventTouchUpInside];
    [self.navigationController.navigationBar addSubview:transparentButton];


}

Agora forneça uma funcionalidade conforme necessário no seguinte método:

-(void)backAction:(UIBarButtonItem *)sender {
    //Your functionality
}

Tudo o que faz é cobrir o botão Voltar com um botão transparente;)

Sarasranglt
fonte
3

Substituindo navigationBar (_ navigationBar: shouldPop) : Essa não é uma boa ideia, mesmo que funcione. para mim, gerou falhas aleatórias ao navegar de volta. Aconselho que você apenas substitua o botão Voltar removendo o backButton padrão do navigationItem e criando um botão Voltar personalizado como abaixo:

override func viewDidLoad(){
   super.viewDidLoad()
   
   navigationItem.leftBarButton = .init(title: "Go Back", ... , action: #selector(myCutsomBackAction) 

   ...
 
}

========================================

Com base em respostas anteriores com uialert em Swift5 em um assíncrono maneira


protocol NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress(_ completion: @escaping (Bool) -> ())
}

extension UINavigationController: UINavigationBarDelegate {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
      
        if viewControllers.count < navigationBar.items!.count {
            return true
        }
        
        // Check if we have a view controller that wants to respond to being popped
        
        if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
            
            viewController.shouldPopOnBackButtonPress { shouldPop in
                if (shouldPop) {
                    /// on confirm => pop
                    DispatchQueue.main.async {
                        self.popViewController(animated: true)
                    }
                } else {
                    /// on cancel => do nothing
                }
            }
            /// return false => so navigator will cancel the popBack
            /// until user confirm or cancel
            return false
        }else{
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        }
        return true
    }
}

No seu controlador


extension MyController: NavigationControllerBackButtonDelegate {
    
    func shouldPopOnBackButtonPress(_ completion: @escaping (Bool) -> ()) {
    
        let msg = "message"
        
        /// show UIAlert
        alertAttention(msg: msg, actions: [
            
            .init(title: "Continuer", style: .destructive, handler: { _ in
                completion(true)
            }),
            .init(title: "Annuler", style: .cancel, handler: { _ in
                completion(false)
            })
            ])
   
    }

}
Brahimm
fonte
Você pode fornecer detalhes sobre o que está acontecendo com a verificação if viewControllers.count <navigationBar.items! .Count {return true}, por favor?
H4Hugo 9/10/19
// impede de um problema de sincronização de popping muitos itens de navegação // e visualizar os controladores não suficientes ou vice-versa de batida incomum
brahimm
2

Não acredito que isso seja possível, facilmente. A única maneira que eu acredito para contornar isso é criar sua própria imagem de seta do botão Voltar para colocar lá em cima. Foi frustrante para mim no começo, mas vejo por que, por uma questão de consistência, foi deixado de fora.

Você pode se aproximar (sem a seta) criando um botão regular e ocultando o botão voltar padrão:

self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:@"Servers" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];
self.navigationItem.hidesBackButton = YES;
Meltemi
fonte
2
Sim, o problema é que eu quero que se parecesse com o botão normal de volta, só precisa dele para chamar minha ação personalizada primeiro ...
Papagaios
2

Há uma maneira mais fácil por apenas subclasse o método delegado do UINavigationBare substituir o ShouldPopItemmétodo .

jazzyjef2002
fonte
Eu acho que você quer dizer subclasse da classe UINavigationController e implementar um método shouldPopItem. Isso está funcionando bem para mim. No entanto, esse método não deve simplesmente retornar SIM ou NÃO, como seria de esperar. Uma explicação e solução está disponível aqui: stackoverflow.com/a/7453933/462162
arlomedia
2

De acordo com os documentos oficiais da Apple, https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html , devemos evitar isso.

"Se o nome de um método declarado em uma categoria for o mesmo que um método na classe original, ou um método em outra categoria na mesma classe (ou mesmo em uma superclasse), o comportamento será indefinido quanto à implementação de método usada em tempo de execução. É menos provável que isso seja um problema se você estiver usando categorias com suas próprias classes, mas pode causar problemas ao usar categorias para adicionar métodos às classes padrão Cocoa ou Cocoa Touch ".

user2612791
fonte
2

Usando Swift:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}
Murray Sagal
fonte
No iOS 10, e talvez mais cedo, isso não está mais funcionando.
Murray Sagal
2

Aqui está a versão Swift 3 da resposta do @oneway para capturar o evento do botão voltar da barra de navegação antes de ser acionado. Como UINavigationBarDelegatenão pode ser usado UIViewController, você precisa criar um delegado que será acionado quando navigationBar shouldPopfor chamado.

@objc public protocol BackButtonDelegate {
      @objc optional func navigationShouldPopOnBackButton() -> Bool 
}

extension UINavigationController: UINavigationBarDelegate  {

    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {

        if viewControllers.count < (navigationBar.items?.count)! {                
            return true
        }

        var shouldPop = true
        let vc = self.topViewController

        if vc.responds(to: #selector(vc.navigationShouldPopOnBackButton)) {
            shouldPop = vc.navigationShouldPopOnBackButton()
        }

        if shouldPop {
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        } else {
            for subView in navigationBar.subviews {
                if(0 < subView.alpha && subView.alpha < 1) {
                    UIView.animate(withDuration: 0.25, animations: {
                        subView.alpha = 1
                    })
                }
            }
        }

        return false
    }
}

E então, no seu controlador de exibição, adicione a função delegar:

class BaseVC: UIViewController, BackButtonDelegate {
    func navigationShouldPopOnBackButton() -> Bool {
        if ... {
            return true
        } else {
            return false
        }        
    }
}

Percebi que muitas vezes queremos adicionar um controlador de alerta para que os usuários decidam se desejam voltar. Se assim for, você pode sempre return falseem navigationShouldPopOnBackButton()função e fechar o controlador de vista, fazendo algo parecido com isto:

func navigationShouldPopOnBackButton() -> Bool {
     let alert = UIAlertController(title: "Warning",
                                          message: "Do you want to quit?",
                                          preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { UIAlertAction in self.yes()}))
            alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { UIAlertAction in self.no()}))
            present(alert, animated: true, completion: nil)
      return false
}

func yes() {
     print("yes")
     DispatchQueue.main.async {
            _ = self.navigationController?.popViewController(animated: true)
        }
}

func no() {
    print("no")       
}
Lawliet
fonte
Estou recebendo um erro: Value of type 'UIViewController' has no member 'navigationShouldPopOnBackButton' quando tento compilar seu código, para a linha if vc.responds(to: #selector(v...Além disso, o self.topViewControllerretorna um opcional e há um aviso para isso também.
Sankar
FWIW, eu corrigi esse código fazendo: let vc = self.topViewController as! MyViewControllere parece funcionar bem até agora. Se você acredita que é uma alteração correta, você pode editar o código. Além disso, se você acha que isso não deve ser feito, ficarei feliz em saber o porquê. Obrigado por este código. Você provavelmente deve escrever um post sobre isso, pois esta resposta está oculta conforme os votos.
precisa
@SankarP A razão pela qual você recebeu esse erro é que você MyViewControllerpode não estar em conformidade BackButtonDelegate. Em vez de forçar a desembrulhar, você deve fazer guard let vc = self.topViewController as? MyViewController else { return true }para evitar uma possível falha.
Lawliet
Obrigado. Eu acho que a declaração de guarda deve se tornar: guard let vc = self.topViewController as? MyViewController else { self.popViewController(animated: true) return true }para garantir que a tela esteja se movendo para a página certa, caso não possa ser projetada corretamente. Entendo agora que a navigationBarfunção é chamada em todos os VCs e não apenas no viewcontroller em que esse código existe. Pode ser bom atualizar também o código na sua resposta? Obrigado.
Sankar
2

Versão Swift 4 iOS 11.3:

Isso se baseia na resposta de kgaidis em https://stackoverflow.com/a/34343418/4316579

Não tenho certeza de quando a extensão parou de funcionar, mas no momento em que escrevemos (Swift 4), parece que a extensão não será mais executada, a menos que você declare a conformidade com o UINavigationBarDelegate, conforme descrito abaixo.

Espero que isso ajude as pessoas que estão se perguntando por que sua extensão não funciona mais.

extension UINavigationController: UINavigationBarDelegate {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {

    }
}
Edward L.
fonte
1

Usando as variáveis ​​de destino e ação que você está deixando no momento 'nil', você poderá conectar suas caixas de diálogo de salvamento para que sejam chamadas quando o botão for "selecionado". Cuidado, isso pode ser acionado em momentos estranhos.

Concordo principalmente com o Amagrammer, mas não acho que seria tão difícil fazer o botão com a seta personalizada. Gostaria apenas de mudar o nome do botão Voltar, tirar uma captura de tela, fazer o photoshop do tamanho necessário e ter essa imagem na parte superior do botão.

TahoeWolverine
fonte
Concordo que você poderia fazer o photoshop e acho que poderia fazer isso se realmente quisesse, mas agora decidi mudar a aparência e sentir um pouquinho para que isso funcionasse da maneira que eu queria.
31710 John Ballinger
Sim, exceto que as ações não são acionadas quando são anexadas ao backBarButtonItem. Não sei se isso é um bug ou um recurso; é possível que nem a Apple saiba. Quanto ao exercício de photoshop, novamente, eu desconfiaria que a Apple rejeitasse o aplicativo por usar indevidamente um símbolo canônico.
Amagrammer
Atenção: esta resposta foi mesclada a partir de uma duplicata.
21913 Shog9
1

Você pode tentar acessar o item do botão direito NavigationBars e definir sua propriedade seletora ... aqui está uma referência UIBarButtonItem , outra coisa, se isso não funcionar, o que definirá o trabalho, configure o item do botão direito da barra de navegação para um UIBarButtonItem personalizado que você criar e definir seu seletor ... espero que isso ajude

Daniel
fonte
Atenção: esta resposta foi mesclada a partir de uma duplicata.
21913 Shog9
1

Para um formulário que exija entrada do usuário como essa, eu recomendaria invocá-lo como um "modal" em vez de parte da sua pilha de navegação. Dessa forma, eles precisam cuidar dos negócios no formulário e validá-los e descartá-los usando um botão personalizado. Você pode até criar uma barra de navegação que tenha a mesma aparência do resto do seu aplicativo, mas que lhe dê mais controle.

Travis M.
fonte
Atenção: esta resposta foi mesclada a partir de uma duplicata.
21913 Shog9
1

Para interceptar o botão Voltar, basta cobri-lo com um UIControl transparente e interceptar os toques.

@interface MyViewController : UIViewController
{
    UIControl   *backCover;
    BOOL        inhibitBackButtonBOOL;
}
@end

@implementation MyViewController
-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    // Cover the back button (cannot do this in viewWillAppear -- too soon)
    if ( backCover == nil ) {
        backCover = [[UIControl alloc] initWithFrame:CGRectMake( 0, 0, 80, 44)];
#if TARGET_IPHONE_SIMULATOR
        // show the cover for testing
        backCover.backgroundColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.15];
#endif
        [backCover addTarget:self action:@selector(backCoverAction) forControlEvents:UIControlEventTouchDown];
        UINavigationBar *navBar = self.navigationController.navigationBar;
        [navBar addSubview:backCover];
    }
}

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

    [backCover removeFromSuperview];
    backCover = nil;
}

- (void)backCoverAction
{
    if ( inhibitBackButtonBOOL ) {
        NSLog(@"Back button aborted");
        // notify the user why...
    } else {
        [self.navigationController popViewControllerAnimated:YES]; // "Back"
    }
}
@end
Jeff
fonte
Atenção: esta resposta foi mesclada a partir de uma duplicata.
Shog9
1

Pelo menos no Xcode 5, existe uma solução simples e muito boa (não perfeita). No IB, arraste um Item do botão Bar para fora do painel Utilitários e solte-o no lado esquerdo da barra de navegação, onde estaria o botão Voltar. Defina o rótulo como "Voltar". Você terá um botão de funcionamento que poderá vincular ao seu IBAction e fechar o seu viewController. Estou fazendo algum trabalho e, em seguida, acionando um desenrolar segue e funciona perfeitamente.

O que não é ideal é que esse botão não receba a seta <e não leve adiante o título anterior dos VCs, mas acho que isso pode ser gerenciado. Para meus propósitos, defino o novo botão Voltar como um botão "Concluído", para que seu objetivo seja claro.

Você também acaba com dois botões Voltar no navegador IB, mas é fácil rotulá-lo para maior clareza.

insira a descrição da imagem aqui

Dan Loughney
fonte
1

Rápido

override func viewWillDisappear(animated: Bool) {
    let viewControllers = self.navigationController?.viewControllers!
    if indexOfArray(viewControllers!, searchObject: self) == nil {
        // do something
    }
    super.viewWillDisappear(animated)
}

func indexOfArray(array:[AnyObject], searchObject: AnyObject)-> Int? {
    for (index, value) in enumerate(array) {
        if value as UIViewController == searchObject as UIViewController {
            return index
        }
    }
    return nil
}
zono
fonte
1

Essa abordagem funcionou para mim (mas o botão "Voltar" não terá o sinal "<"):

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIBarButtonItem* backNavButton = [[UIBarButtonItem alloc] initWithTitle:@"Back"
                                                                      style:UIBarButtonItemStyleBordered
                                                                     target:self
                                                                     action:@selector(backButtonClicked)];
    self.navigationItem.leftBarButtonItem = backNavButton;
}

-(void)backButtonClicked
{
    // Do something...
    AppDelegate* delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
    [delegate.navController popViewControllerAnimated:YES];
}
Ivan
fonte
1

Versão rápida da resposta de @ onegray

protocol RequestsNavigationPopVerification {
    var confirmationTitle: String { get }
    var confirmationMessage: String { get }
}

extension RequestsNavigationPopVerification where Self: UIViewController {
    var confirmationTitle: String {
        return "Go back?"
    }

    var confirmationMessage: String {
        return "Are you sure?"
    }
}

final class NavigationController: UINavigationController {

    func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {

        guard let requestsPopConfirm = topViewController as? RequestsNavigationPopVerification else {
            popViewControllerAnimated(true)
            return true
        }

        let alertController = UIAlertController(title: requestsPopConfirm.confirmationTitle, message: requestsPopConfirm.confirmationMessage, preferredStyle: .Alert)

        alertController.addAction(UIAlertAction(title: "Cancel", style: .Cancel) { _ in
            dispatch_async(dispatch_get_main_queue(), {
                let dimmed = navigationBar.subviews.flatMap { $0.alpha < 1 ? $0 : nil }
                UIView.animateWithDuration(0.25) {
                    dimmed.forEach { $0.alpha = 1 }
                }
            })
            return
        })

        alertController.addAction(UIAlertAction(title: "Go back", style: .Default) { _ in
            dispatch_async(dispatch_get_main_queue(), {
                self.popViewControllerAnimated(true)
            })
        })

        presentViewController(alertController, animated: true, completion: nil)

        return false
    }
}

Agora, em qualquer controlador, apenas se conforme RequestsNavigationPopVerificatione esse comportamento é adotado por padrão.

Adam Waite
fonte
1

Usar isMovingFromParentViewController

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(true)

    if self.isMovingFromParentViewController {
        // current viewController is removed from parent
        // do some work
    }
}
Herrk
fonte
Você poderia explicar melhor como isso prova que o botão Voltar foi acionado?
Murray Sagal
Isso é simples, mas funciona apenas se você tiver certeza de voltar a essa exibição de qualquer exibição filho que possa carregar. Se o filho pular essa visualização quando retornar ao pai, seu código não será chamado (a visualização já havia desaparecido sem ter sido movida do pai). Mas esse é o mesmo problema com apenas manipular eventos no acionamento do botão Voltar, conforme solicitado pelo OP. Portanto, esta é uma resposta simples para sua pergunta.
CMont
Isso é super simples e elegante. Eu amo isso. Apenas um problema: isso também será acionado se o usuário passar o dedo para voltar, mesmo se cancelar no meio. Talvez uma solução melhor seria colocar esse código viewDidDisappear. Dessa forma, ele só será acionado quando a visualização desaparecer.
Phontaine Judd
1

A resposta de @William está correta, no entanto, se o usuário iniciar um gesto de deslizar para voltar, o viewWillDisappearmétodo será chamado e ainda selfnão estará na pilha de navegação (ou seja, self.navigationController.viewControllersnão conterá self), mesmo que o deslize não foi concluído e o controlador de exibição não foi realmente exibido. Assim, a solução seria:

  1. Desative o gesto de deslizar para voltar viewDidAppeare permita apenas o uso do botão voltar usando:

    if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)])
    {
        self.navigationController.interactivePopGestureRecognizer.enabled = NO;
    }
  2. Ou simplesmente use viewDidDisappear, da seguinte maneira:

    - (void)viewDidDisappear:(BOOL)animated
    {
        [super viewDidDisappear:animated];
        if (![self.navigationController.viewControllers containsObject:self])
        {
            // back button was pressed or the the swipe-to-go-back gesture was
            // completed. We know this is true because self is no longer
            // in the navigation stack.
        }
    }
boherna
fonte
0

A solução que encontrei até agora não é muito boa, mas funciona para mim. Tomando esta resposta , 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];
Ferran Maylinch
fonte
0

Foi encontrada uma nova maneira de fazer isso:

Objetivo-C

- (void)didMoveToParentViewController:(UIViewController *)parent{
    if (parent == NULL) {
        NSLog(@"Back Pressed");
    }
}

Rápido

override func didMoveToParentViewController(parent: UIViewController?) {
    if parent == nil {
        println("Back Pressed")
    }
}
Ashish Kakkad
fonte