iphone - por - Configuración de acción para el botón Atrás en el controlador de navegación
control por boton iphone (28)
Swift 4 iOS 11.3 Version:
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 en el momento de escribir esto (Swift 4), parece que la extensión ya no se ejecutará a menos que declare la conformidad de 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 {
}
}
Estoy intentando 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 a través del atributo de botón de retroceso no les presta atención y solo aparece 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 configuro a través del leftBarButtonItem en el navigationItem llama a mi acción, sin embargo, luego el botón se ve como uno simple redondo en lugar del flechazo 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 manera de sobrescribir la acción de respaldo predeterminada, o hay un método que siempre se llama al salir de una vista (viewDidUnload no hace eso)?
Versión Swift:
(de https://.com/a/19132881/826435 )
En su controlador de vista solo se ajusta a un protocolo y realiza cualquier acción que necesite:
extension MyViewController: NavigationControllerBackButtonDelegate {
func shouldPopOnBackButtonPress() -> Bool {
performSomeActionOnThePressOfABackButton()
return false
}
}
Luego crea una clase, dijera NavigationController+BackButton
, y simplemente copia y pega 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
}
}
A diferencia de Amagrammer, dijo, es posible. Tienes que subclasificar tu navigationController. Expliqué todo aquí (incluido el código de ejemplo): http://www.hanspinckaers.com/custom-action-on-back-button-uinavigationcontroller
Al menos en Xcode 5, hay una solución simple y bastante buena (no perfecta). En IB, arrastre un elemento de botón de barra fuera del panel de 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ás un botón de funcionamiento que puedes vincular a tu IBAction y cerrar tu viewController. Estoy haciendo un trabajo y luego desencadenar un ciclo de desenrollado y funciona perfectamente.
Lo que no es ideal es que este botón no obtenga la <flecha y no lleve adelante el título anterior de VCs, pero creo que esto se puede gestionar. Para mis propósitos, configuro el nuevo botón Atrás para que sea un botón "Listo", por lo que su propósito es claro.
También termina con dos botones Atrás en el navegador IB, pero es bastante fácil etiquetarlo para mayor claridad.
Al utilizar las variables de objetivo y acción que actualmente está dejando ''nil'', debería poder cablear sus diálogos de guardado para que se llamen 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 el botón con la flecha personalizada. 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 tendría que ser la imagen en la parte superior de su botón.
Aquí está la versión de 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 dispare. Como UINavigationBarDelegate
no se puede usar para UIViewController
, debe crear un delegado que se activará cuando se shouldPop
navigationBar
shouldPop
.
@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 di cuenta de que a menudo queremos agregar un controlador de alerta para que los usuarios decidan si quieren regresar. Si es así, siempre puede return false
en la función navigationShouldPopOnBackButton()
y cerrar su controlador de vista haciendo 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")
}
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
}
}
Encontré 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")
}
}
Encontré una solución que también conserva el estilo del botón Atrás. 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 Atrás con un botón transparente;)
Esta técnica le permite cambiar el texto del botón "Atrás" sin afectar el título de ninguno de los controles de vista o ver el texto del botón Atrás cambiar durante la animación.
Agregue esto al método init en el controlador de vista llamante :
UIBarButtonItem *temporaryBarButtonItem = [[UIBarButtonItem alloc] init];
temporaryBarButtonItem.title = @"Back";
self.navigationItem.backBarButtonItem = temporaryBarButtonItem;
[temporaryBarButtonItem release];
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];
}
Hay una manera más fácil de subclasificar el método de delegado de UINavigationBar
y anular el método ShouldPopItem
.
Intenta poner esto en el controlador de vista donde deseas 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];
}
La respuesta de @William es correcta, sin embargo, si el usuario inicia un gesto de deslizar para volver, se llama al método viewWillDisappear e incluso ''self'' no estará en la pila de navegación (es decir, self.navigationController.viewControllers won ''t contiene'' self ''), incluso si el deslizamiento no se completa y el controlador de vista no se ha reventado realmente. Por lo tanto, la solución sería:
Deshabilite el gesto de deslizar para volver atrás en viewDidAppear y solo permita el uso del botón Atrás, usando:
if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) { self.navigationController.interactivePopGestureRecognizer.enabled = NO; }
O simplemente use viewDidDisappear en 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. } }
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 , deberíamos evitar hacerlo.
"Si el nombre de un método declarado en una categoría es igual a 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é método de implementación se usa en tiempo de ejecución. Es menos probable que sea un problema si está utilizando 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 ".
La solución que he encontrado hasta ahora no es muy buena, pero funciona para mí. Tomando esta answer , también compruebo 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 configurarlo en SÍ antes de hacer clic en programación:
self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];
No creo que esto sea posible, fácilmente. La única forma en que creo para evitar esto es hacer tu propia imagen de flecha de botón hacia atrás para colocarla allí. Al principio fue frustrante para mí, pero veo por qué, por consistencia, se dejó de lado.
Puede acercarse (sin la flecha) creando un botón normal y ocultando el botón Atrás predeterminado:
self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:@"Servers" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];
self.navigationItem.hidesBackButton = YES;
No es posible hacerlo directamente. Hay un par de alternativas:
- Cree su propio
UIBarButtonItem
personalizado que se valida al tocar y aparece si pasa la prueba - Valide los contenidos del campo de formulario utilizando un método delegado
UITextField
, como-textFieldShouldReturn:
que se-textFieldShouldReturn:
después de presionar el botónReturn
oDone
en 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. Entonces debes usar una imagen o ir con un botón de estilo regular.
La segunda opción es agradable porque recupera el campo de texto en el método de 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 del delegado.
Para interceptar el botón Atrás, simplemente cúbrelo con un UIControl transparente e intercepta 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
Para un formulario que requiera la entrada del usuario de esta manera, recomendaría invocarlo como un "modal" en lugar de una parte de su pila de navegación. De esta forma, deben ocuparse de los negocios en el formulario, luego puede validarlo y descartarlo mediante un botón personalizado. Incluso puede diseñar una barra de navegación que se ve igual que el resto de su aplicación, pero le da más control.
Por algunas razones de enhebrado, 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 detallar esto aquí en caso de que esto pueda evitar horas de engaños para alguien más. El truco es realmente fácil: simplemente agrega un UIButton transparente como una subvista a tu UINavigationBar, y configura tus selectores para él como si fuera el botón real. Aquí hay un ejemplo usando Monotouch y C #, pero la traducción a Object-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é el color de fondo en azul ... ¡Y se muestra detrás del botón Atrás! De todos modos, todavía capta cualquier toque que apunte al botón original.
Puede intentar acceder al elemento del botón derecho de NavigationBars y establecer su propiedad de selector ... aquí está una referencia UIBarButtonItem referencia , otra cosa si este doenst trabajo que def funcionar es, establecer el elemento del botón derecho de la barra de navegación a un UIBarButtonItem personalizado que usted crea y establece su selector ... espero que esto ayude
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
}
Usando Swift:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if self.navigationController?.topViewController != self {
print("back button tapped")
}
}
Utilice isMovingFromParentViewController
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(true)
if self.isMovingFromParentViewController {
// current viewController is removed from parent
// do some work
}
}
UIViewController-BackButtonHandler extensión UIViewController-BackButtonHandler . No necesita subclasificar nada, simplemente colócalo en tu proyecto y anula el método navigationShouldPopOnBackButton
en la clase UIViewController
:
-(BOOL) navigationShouldPopOnBackButton {
if(needsShowConfirmation) {
// Show confirmation alert
// ...
return NO; // Ignore ''Back'' button this time
}
return YES; // Process ''Back'' button click and Pop view controler
}
La manera más fácil
Puede usar los métodos de delegado de UINavigationController. El método willShowViewController
se llama cuando se presiona el botón Atrás de su VC. Haga lo que desee cuando vuelva a presionar btn
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
Versión Swift 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 con RequestsNavigationPopVerification
y este comportamiento se adopta de forma predeterminada.