Configuración de acción para el botón Atrás en el controlador de navegación

180

Estoy tratando de sobrescribir la acción predeterminada del botón Atrás en un controlador de navegación. Le proporcioné a un objetivo una acción en el botón personalizado. Lo extraño es que al asignarlo, aunque el atributo de botón de retroceso no les presta atención, solo muestra la vista actual y vuelve a la raíz:

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

Tan pronto como lo configuré leftBarButtonItemen el navigationItem, llama a mi acción, sin embargo, el botón parece simple y redondo en lugar del de flecha hacia atrás:

self.navigationItem.leftBarButtonItem = backButton;

¿Cómo puedo hacer que llame a mi acción personalizada antes de volver a la vista raíz? ¿Hay alguna forma de sobrescribir la acción de retroceso predeterminada, o hay un método que siempre se llama al salir de una vista ( viewDidUnloadno hace eso)?

Loros
fuente
acción: @selector (inicio)]; necesita un: después de la acción del selector: @selector (inicio :)]; de lo contrario no funcionará
PartySoft
77
@PartySoft Eso no es cierto a menos que el método se declare con los dos puntos. Es perfectamente válido tener botones de selección de llamadas que no toman ningún parámetro.
mbm29414
3
¿Por qué Apple no proporcionaría un botón con un estilo con forma de botón de retroceso? Parece bastante obvio.
JohnK
Mira la solución en este hilo
Jiri Volejnik

Respuestas:

363

Intente poner esto en el controlador de vista donde desea detectar la prensa:

-(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
fuente
1
esta es una solución
ingeniosa
66
+1 gran truco, pero no ofrece control sobre la animación del pop
matm
3
No funciona para mí si envío un mensaje al delegado a través de un botón y el delegado abre el controlador; esto todavía se dispara.
SAHM
21
Otro problema es que no puede diferenciar si el usuario presionó el botón Atrás o si programáticamente llamó [self.navigationController popViewControllerAnimated: YES]
Chase Roberts el
10
Solo un FYI: versión rápida:if (find(self.navigationController!.viewControllers as! [UIViewController],self)==nil)
hEADcRASH
177

He implementado la extensión UIViewController-BackButtonHandler . No necesita subclasificar nada, solo colóquelo en su proyecto y anule el navigationShouldPopOnBackButtonmétodo en UIViewControllerclase:

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

Descargue la aplicación de muestra .

onegray
fuente
16
Esta es la solución más limpia que he visto, mejor y más simple que usar su propio UIButton personalizado. ¡Gracias!
ramirogm
44
El navigationBar:shouldPopItem:no es un método privado ya que es parte del UINavigationBarDelegateprotocolo.
onegray
1
pero ¿UINavigationController ya implementa el delegado (para volver YES)? o lo hará en el futuro? subclasificar es probablemente una opción más segura
Sam
8
Acabo de implementar esto (BTW bastante bueno) en iOS 7.1 y noté que después de regresar NOel botón de retroceso permanece en un estado deshabilitado (visualmente, porque todavía recibe y reacciona a eventos táctiles). Lo solucioné agregando una elsedeclaración al shouldPopcheque y recorriendo las subvistas de la barra de navegación, y estableciendo el alphavalor nuevamente en 1 si es necesario dentro de un bloque de animación: gist.github.com/idevsoftware/9754057
boliva
2
Esta es una de las mejores extensiones que he visto. Muchas gracias.
Srikanth
42

A diferencia de Amagrammer, dijo, es posible. Tienes que subclasificar tu navigationController. Le expliqué todo aquí (incluido el código de ejemplo).

HansPinckaers
fuente
La documentación de Apple ( developer.apple.com/iphone/library/documentation/UIKit/… ) dice que "Esta clase no está diseñada para subclasificar". Aunque no estoy seguro de lo que quieren decir con esto, podrían significar "normalmente no deberías hacer eso", o podrían decir "rechazaremos tu aplicación si te metes con nuestro controlador" ...
Kuba Suder
Esta es ciertamente la única forma de hacerlo. ¡Ojalá pudiera darte más puntos, Hans!
Adam Eberbach
1
¿Puedes evitar que una vista salga usando este método? ¿Qué haría que regresara el método popViewControllerAnimated si quisiera que la vista no salga?
JosephH
1
Si puedes. Simplemente no llame al método de superclase en su implementación, ¡tenga en cuenta! No debe hacer eso, el usuario espera volver a la navegación. Lo que puede hacer es solicitar una confirmación. De acuerdo con la documentación de Apple, popViewController devuelve: "El controlador de vista que se sacó de la pila". Entonces, cuando no aparece nada, debe volver nulo;
HansPinckaers
1
@HansPickaers Creo que su respuesta sobre evitar que una vista salga puede ser algo incorrecta. Si visualizo un mensaje de 'confirmación' de la implementación de subclases de popViewControllerAnimated :, NavigationBar todavía anima un nivel en el árbol, independientemente de lo que devuelva. Esto parece deberse a que al hacer clic en el botón Atrás se debe llamar a ItemDeNavegación en la barra de navegación. Estoy regresando nil desde mi método de subclases como se recomienda.
invierno profundo
15

Versión rápida:

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

En su controlador de vista, simplemente se ajusta a un protocolo y realiza cualquier acción que necesite:

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

Luego cree una clase, digamos NavigationController+BackButton, y simplemente copie y pegue el siguiente código:

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
fuente
Tal vez me perdí algo, pero no funciona para mí, el método performSomeActionOnThePressOfABackButton de la extensión nunca se llama
Turvy
@FlorentBreton tal vez un malentendido? shouldPopOnBackButtonPressdebe llamarse siempre que no haya errores. performSomeActionOnThePressOfABackButtones solo un método inventado que no existe.
kgaidis
Lo entendí, es por eso que creé un método performSomeActionOnThePressOfABackButtonen mi controlador para ejecutar una acción específica cuando se presiona el botón de retroceso, pero este método nunca se llamó, la acción es un retorno normal
Turvy
1
No funciona para mí tampoco. El método shouldPop nunca se llama. ¿Pusiste un delegado en alguna parte?
Tim Autin
@TimAutin Acabo de probar esto nuevamente y parece que algo ha cambiado. Parte clave para comprender que en a UINavigationController, navigationBar.delegatese establece en el controlador de navegación. Entonces los métodos DEBEN ser llamados. Sin embargo, en Swift, no puedo hacer que se llamen, incluso en una subclase. Sin embargo, conseguí que se llamaran en Objective-C, por lo que solo usaría la versión Objective-C por ahora. Podría ser un error de Swift.
kgaidis
5

No es posible hacerlo directamente. Hay un par de alternativas:

  1. Cree su propia costumbre UIBarButtonItemque se valida al tocar y hacer estallar si la prueba pasa
  2. Valide el contenido del campo de formulario utilizando un UITextFieldmétodo de delegado, como -textFieldShouldReturn:, que se llama después de presionar el botón Returno Doneen el teclado

La desventaja de la primera opción es que no se puede acceder al estilo de flecha hacia la izquierda del botón Atrás desde un botón de barra personalizado. Por lo tanto, debe usar una imagen o ir con un botón de estilo normal.

La segunda opción es buena porque recupera el campo de texto en el método delegado, por lo que puede orientar su lógica de validación al campo de texto específico enviado al método de devolución de llamada delegado.

Alex Reynolds
fuente
5

Por algunas razones, la solución mencionada por @HansPinckaers no era adecuada para mí, pero encontré una manera más fácil de tocar el botón Atrás, y quiero fijar esto aquí en caso de que esto pueda evitar horas de engaños para alguien más. El truco es realmente fácil: simplemente agregue un botón UIB transparente como una subvista a su UINavigationBar, y configure sus selectores para él como si fuera el botón real. Aquí hay un ejemplo usando Monotouch y C #, pero la traducción a Objective-C no debería ser demasiado 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
    }
}

Dato curioso: para fines de prueba y para encontrar buenas dimensiones para mi botón falso, configuré su color de fondo en azul ... ¡Y se muestra detrás del botón Atrás! De todos modos, todavía atrapa cualquier toque que apunte al botón original.

psicópata
fuente
3

Esta técnica le permite cambiar el texto del botón "atrás" sin afectar el título de ninguno de los controladores de vista ni ver el cambio del texto del botón atrás durante la animación.

Agregue esto al método init en el controlador de vista de llamada :

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

La manera más fácil

Puede usar los métodos delegados de UINavigationController. Se willShowViewControllerllama al método cuando se presiona el botón Atrás de su VC. Haga lo que quiera cuando se presione Btn.

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
Zar E Ahmer
fuente
Asegúrese de que su controlador de vista se establezca como el delegado del control de navegación heredado y que cumpla con el protocolo UINavigationControllerDelegate
Justin Milo
3

Aquí está mi solución Swift. En su subclase de UIViewController, anule el 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
fuente
Anular el método navigationShouldPopOnBackButton en UIViewController no funciona: el programa ejecuta el método principal, no el sobrescrito. ¿Alguna solución para eso? ¿Alguien tiene el mismo problema?
Pawel Cala
que va todo el camino a volver rootview si es una declaración verdadera
Pawriwes
@Pawriwes Aquí hay una solución que escribí que parece funcionar para mí: stackoverflow.com/a/34343418/826435
kgaidis
3

Encontró una solución que conserva el estilo del botón de retroceso también. Agregue el siguiente método a su controlador de vista.

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


}

Ahora proporcione una funcionalidad según sea necesario en el siguiente método:

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

Todo lo que hace es cubrir el botón de retroceso con un botón transparente;)

Sarasranglt
fuente
3

Anulación de navigationBar (_ navigationBar: shouldPop) : esta no es una buena idea, incluso si funciona. para mí generó bloqueos aleatorios al navegar de regreso. Le aconsejo que anule el botón de retroceso quitando el botón de retroceso predeterminado de navigationItem y creando un botón de retroceso personalizado como el siguiente:

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

   ...
 
}

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

Sobre la base de las respuestas anteriores con UIAlert en Swift5 en un asíncrono camino


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

En su 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
fuente
¿Puede proporcionar detalles sobre lo que está sucediendo con el control if viewControllers.count <navigationBar.items! .Count {return true} por favor?
H4Hugo
// Evita un problema de sincronización de hacer estallar demasiados elementos de navegación // y no hay suficientes controladores de vista o viceversa de golpes inusuales
brahimm
2

No creo que esto sea posible, fácilmente. La única forma en que creo evitar esto es hacer su propia imagen de flecha del botón de retroceso para colocarla allí. Al principio fue frustrante para mí, pero veo por qué, por razones de coherencia, se dejó de lado.

Puede acercarse (sin la flecha) creando un botón normal y ocultando el botón de retroceso predeterminado:

self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:@"Servers" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];
self.navigationItem.hidesBackButton = YES;
Meltemi
fuente
2
Sí, el problema es que quiero que se vea como el botón de retroceso normal, solo necesito que llame primero a mi acción personalizada ...
Parrots
2

Hay una manera más fácil simplemente subclasificando el método delegado del UINavigationBary anula el ShouldPopItemmétodo .

jazzyjef2002
fuente
Creo que te refieres a decir subclase la clase UINavigationController e implementar un método shouldPopItem. Eso está funcionando bien para mí. Sin embargo, ese método no debería simplemente devolver SÍ o NO como era de esperar. Una explicación y solución está disponible aquí: stackoverflow.com/a/7453933/462162
arlomedia
2

La solución de onegray no es segura. De acuerdo con los documentos oficiales de Apple, https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html , debemos evitar hacerlo.

"Si el nombre de un método declarado en una categoría es el mismo que un método en la clase original, o un método en otra categoría en la misma clase (o incluso una superclase), el comportamiento no está definido en cuanto a qué implementación de método se utiliza en tiempo de ejecución. Es menos probable que esto sea un problema si usa categorías con sus propias clases, pero puede causar problemas al usar categorías para agregar métodos a las clases estándar de Cocoa o Cocoa Touch ".

usuario2612791
fuente
2

Usando Swift:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}
Murray Sagal
fuente
A partir de iOS 10, y tal vez antes, esto ya no funciona.
Murray Sagal
2

Aquí está la versión Swift 3 de la respuesta de @oneway para capturar el evento del botón de retroceso de la barra de navegación antes de que se active. Como UINavigationBarDelegateno se puede utilizar UIViewController, debe crear un delegado que se activará cuando navigationBar shouldPopse llame.

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

Y luego, en su controlador de vista, agregue la función de delegado:

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

Me he dado cuenta de que a menudo queremos agregar un controlador de alertas para que los usuarios decidan si quieren regresar. Si es así, siempre se puede return falseen la navigationShouldPopOnBackButton()función y cerrar el controlador de vista al hacer algo como esto:

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
fuente
Recibo un error: Value of type 'UIViewController' has no member 'navigationShouldPopOnBackButton' cuando trato de compilar su código, para la línea if vc.responds(to: #selector(v...Además, el self.topViewControllerdevuelve un opcional y también hay una advertencia para eso.
Sankar
FWIW, he arreglado ese código haciendo: let vc = self.topViewController as! MyViewControllery parece funcionar bien hasta ahora. Si cree que es un cambio correcto, puede editar el código. Además, si siente que no debe hacerse, me alegrará saber por qué. Gracias por este código. Probablemente debería escribir una publicación de blog sobre esto, ya que esta respuesta está oculta según los votos.
Sankar
@SankarP La razón por la que obtuvo ese error es que es MyViewControllerposible que no se ajuste BackButtonDelegate. En lugar de forzar el desenvolvimiento, debe hacerlo guard let vc = self.topViewController as? MyViewController else { return true }para evitar un posible choque.
Lawliet
Gracias. Creo que la declaración de guardia debería ser: guard let vc = self.topViewController as? MyViewController else { self.popViewController(animated: true) return true }para asegurarse de que la pantalla se mueva a la página correcta en caso de que no se pueda emitir correctamente. Ahora entiendo que la navigationBarfunción se llama en todos los VC y no solo en el controlador de vista donde existe este código. ¿Puede ser bueno actualizar el código en su respuesta también? Gracias.
Sankar
2

Swift 4 iOS 11.3 Versión:

Esto se basa en la respuesta de kgaidis de https://stackoverflow.com/a/34343418/4316579

No estoy seguro de cuándo la extensión dejó de funcionar, pero al momento de escribir esto (Swift 4), parece que la extensión ya no se ejecutará a menos que declare la conformidad UINavigationBarDelegate como se describe a continuación.

Espero que esto ayude a las personas que se preguntan por qué su extensión ya no funciona.

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

    }
}
Edward L.
fuente
1

Al utilizar el objetivo y las variables de acción que está dejando actualmente 'nulo', debería poder conectar sus diálogos de guardado para que se invoquen cuando el botón esté "seleccionado". Cuidado, esto puede activarse en momentos extraños.

Estoy de acuerdo principalmente con Amagrammer, pero no creo que sea tan difícil hacer que el botón con la flecha sea personalizado. Simplemente cambiaría el nombre del botón Atrás, tomaría una captura de pantalla, photoshop el tamaño del botón necesario, y que esa sea la imagen en la parte superior de su botón.

TahoeWolverine
fuente
Estoy de acuerdo en que podría hacer photoshop y creo que podría hacer esto si realmente lo quisiera, pero ahora he decidido cambiar el aspecto y sentir un poquito para que funcione de la manera que quiero.
John Ballinger el
Sí, excepto que las acciones no se activan cuando se adjuntan al backBarButtonItem. No sé si esto es un error o una característica; Es posible que incluso Apple no lo sepa. En cuanto al ejercicio de sesión de fotos, una vez más, desconfiaría de que Apple rechazara la aplicación por usar mal un símbolo canónico.
Amagrammer
Atención: esta respuesta se ha fusionado de un duplicado.
Shog9
1

Puede intentar acceder al elemento del botón derecho de NavigationBars y establecer su propiedad de selector ... aquí hay una referencia UIBarButtonItem referencia , otra cosa si este trabajo que no funcionará es establecer el elemento del botón derecho de la barra de navegación en un elemento UIBarButtonItem personalizado que crear y configurar su selector ... espero que esto ayude

Daniel
fuente
Atención: esta respuesta se ha fusionado de un duplicado.
Shog9
1

Para un formulario que requiere la entrada del usuario como este, recomendaría invocarlo como un "modal" en lugar de parte de su pila de navegación. De esa manera, tienen que ocuparse de los negocios en el formulario, luego puede validarlo y descartarlo con un botón personalizado. Incluso puedes diseñar una barra de navegación que se vea igual que el resto de tu aplicación pero que te dé más control.

Travis M.
fuente
Atención: esta respuesta se ha fusionado de un duplicado.
Shog9
1

Para interceptar el botón Atrás, simplemente cúbralo con un control UIC transparente e intercepte los 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
fuente
Atención: esta respuesta se ha fusionado de un duplicado.
Shog9
1

Al menos en Xcode 5, hay una solución simple y bastante buena (no perfecta). En IB, arrastre un elemento del botón de barra fuera del panel Utilidades y suéltelo en el lado izquierdo de la barra de navegación donde estaría el botón Atrás. Establezca la etiqueta en "Atrás". Tendrá un botón de funcionamiento que puede vincular a su IBAction y cerrar su viewController. Estoy haciendo un trabajo y luego desencadenando una desconexión y funciona perfectamente.

Lo que no es ideal es que este botón no obtiene la flecha <y no lleva adelante el título de VC anterior, pero creo que esto se puede administrar. Para mis propósitos, configuré el nuevo botón Atrás para que sea un botón "Listo" para que su propósito sea claro.

También terminas con dos botones Atrás en el navegador IB, pero es bastante fácil etiquetarlo para mayor claridad.

ingrese la descripción de la imagen aquí

Dan Loughney
fuente
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
fuente
1

Este enfoque funcionó para mí (pero el botón "Atrás" no tendrá el signo "<"):

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

Versión rápida de la respuesta 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
    }
}

Ahora, en cualquier controlador, simplemente cumpla RequestsNavigationPopVerificationy este comportamiento se adopta por defecto.

Adam Waite
fuente
1

Utilizar isMovingFromParentViewController

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

    if self.isMovingFromParentViewController {
        // current viewController is removed from parent
        // do some work
    }
}
herrk
fuente
¿Podría explicar más a fondo cómo esto demuestra que se tocó el botón Atrás?
Murray Sagal
Esto es simple, pero funciona solo si está seguro de volver a esa vista desde cualquier vista secundaria que pueda cargar. Si el niño omite esta vista cuando vuelve al padre, no se llamará a su código (la vista ya había desaparecido sin haberse movido del padre). Pero ese es el mismo problema con solo manejar eventos en el disparador del botón Atrás según lo solicitado por el OP. Entonces esta es una respuesta simple a su pregunta.
CMont
Esto es super simple y elegante. Me encanta. Solo un problema: esto también se disparará si el usuario desliza el dedo para regresar, incluso si cancela a mitad de camino. Quizás una mejor solución sería poner este código viewDidDisappear. De esa manera, solo se disparará una vez que la vista haya desaparecido definitivamente.
Phontaine Judd
1

Sin embargo, la respuesta de @William es correcta, si el usuario inicia un gesto de deslizar hacia atrás viewWillDisappear, se llama al método e incluso selfno estará en la pila de navegación (es decir, self.navigationController.viewControllersno contendrá self), incluso si el deslizamiento no se ha completado y el controlador de vista no aparece realmente. Por lo tanto, la solución sería:

  1. Deshabilite el gesto de deslizar hacia atrás viewDidAppeary solo permita usar el botón Atrás, usando:

    if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)])
    {
        self.navigationController.interactivePopGestureRecognizer.enabled = NO;
    }
  2. O simplemente use viewDidDisappearen su lugar, de la siguiente manera:

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

La solución que he encontrado hasta ahora no es muy agradable, pero funciona para mí. Tomando esta respuesta , también verifico si estoy apareciendo programáticamente o no:

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

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

Debe agregar esa propiedad a su controlador y establecerla en SÍ antes de aparecer mediante programación:

self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];
Ferran Maylinch
fuente
0

Encontramos una nueva forma de hacerlo:

C objetivo

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