C'est peut-être un peu trop tard, mais je voulais aussi le même comportement avant. Et la solution que j'ai choisie fonctionne assez bien dans l'une des applications actuellement sur l'App Store. Comme je n'ai vu personne utiliser une méthode similaire, j'aimerais la partager ici. L'inconvénient de cette solution est qu'elle nécessite un sous-classement UINavigationController
. Bien que l'utilisation de Method Swizzling puisse aider à éviter cela, je ne suis pas allé aussi loin.
Ainsi, le bouton de retour par défaut est en fait géré par UINavigationBar
. Lorsqu'un utilisateur appuie sur le bouton de retour, UINavigationBar
demandez à son délégué s'il doit apparaître en haut UINavigationItem
en appelant navigationBar(_:shouldPop:)
. UINavigationController
implémente réellement ceci, mais il ne déclare pas publiquement qu'il adopte UINavigationBarDelegate
(pourquoi!?). Pour intercepter cet événement, créez une sous-classe de UINavigationController
, déclarez sa conformité UINavigationBarDelegate
et implémentez navigationBar(_:shouldPop:)
. Renvoyez true
si l'élément supérieur doit être sauté. Revenez false
s'il doit rester.
Il y a deux problèmes. La première est que vous devez appeler la UINavigationController
version de navigationBar(_:shouldPop:)
à un moment donné. Mais UINavigationBarController
ne le déclare pas publiquement conforme à UINavigationBarDelegate
, essayer de l'appeler entraînera une erreur de compilation. La solution que j'ai choisie est d'utiliser le runtime Objective-C pour obtenir directement l'implémentation et l'appeler. Veuillez me faire savoir si quelqu'un a une meilleure solution.
L'autre problème est qu'il navigationBar(_:shouldPop:)
est appelé en premier après popViewController(animated:)
si l'utilisateur appuie sur le bouton de retour. L'ordre s'inverse si le contrôleur de vue est sauté en appelant popViewController(animated:)
. Dans ce cas, j'utilise un booléen pour détecter si popViewController(animated:)
est appelé avant, navigationBar(_:shouldPop:)
ce qui signifie que l'utilisateur a tapé sur le bouton de retour.
De plus, je crée une extension UIViewController
pour permettre au contrôleur de navigation de demander au contrôleur de vue s'il doit être affiché si l'utilisateur appuie sur le bouton de retour. Les contrôleurs de vue peuvent revenir false
et effectuer toutes les actions nécessaires et appeler popViewController(animated:)
plus tard.
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
}
}
Et dans vous regardez les contrôleurs, implémentez shouldBePopped(_:)
. Si vous n'implémentez pas cette méthode, le comportement par défaut sera de faire apparaître le contrôleur de vue dès que l'utilisateur appuie sur le bouton de retour comme d'habitude.
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
}
}
Vous pouvez regarder ma démo ici .