Tal vez sea un poco tarde, pero también quería el mismo comportamiento antes. Y la solución con la que elegí funciona bastante bien en una de las aplicaciones que se encuentran actualmente en la App Store. Como no he visto a nadie usar un método similar, me gustaría compartirlo aquí. La desventaja de esta solución es que requiere subclases UINavigationController
. Aunque use el método Swizzling podría ayudar a evitar eso, no fui tan lejos.
Entonces, el botón de retroceso predeterminado es administrado por UINavigationBar
. Cuando un usuario toca el botón de retroceso, UINavigationBar
pregúntele a su delegado si debería abrir la parte superior UINavigationItem
llamando navigationBar(_:shouldPop:)
. UINavigationController
en realidad implementa esto, pero no declara públicamente que lo adopta UINavigationBarDelegate
(¿por qué?). Para interceptar este evento, cree una subclase de UINavigationController
, declare su conformidad UINavigationBarDelegate
e implemente navigationBar(_:shouldPop:)
. Regrese true
si el elemento superior debe aparecer. Regrese false
si debe quedarse.
Hay dos problemas. La primera es que debes llamar a la UINavigationController
versión de navigationBar(_:shouldPop:)
en algún momento. Pero UINavigationBarController
no lo declara públicamente de conformidad UINavigationBarDelegate
, intentar llamarlo resultará en un error de tiempo de compilación. La solución con la que fui es usar el tiempo de ejecución de Objective-C para obtener la implementación directamente y llamarla. Por favor, avíseme si alguien tiene una mejor solución.
El otro problema es que navigationBar(_:shouldPop:)
se llama primero y sigue popViewController(animated:)
si el usuario toca el botón Atrás. El orden se invierte si el controlador de vista se abre al llamar popViewController(animated:)
. En este caso, utilizo un valor booleano para detectar si popViewController(animated:)
se llama antes, lo navigationBar(_:shouldPop:)
que significa que el usuario ha pulsado el botón Atrás.
Además, hago una extensión de UIViewController
para permitir que el controlador de navegación pregunte al controlador de vista si debe aparecer si el usuario toca el botón Atrás. Los controladores de View pueden regresar false
y realizar las acciones necesarias y llamar popViewController(animated:)
más 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
}
}
Y en su vista controladores, implemente shouldBePopped(_:)
. Si no implementa este método, el comportamiento predeterminado será abrir el controlador de vista tan pronto como el usuario toque el botón de retroceso como de costumbre.
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
}
}
Puedes ver mi demo aquí .