Forse è un po 'troppo tardi, ma volevo anche lo stesso comportamento prima. E la soluzione che ho scelto funziona abbastanza bene in una delle app attualmente sull'App Store. Dato che non ho visto nessuno con un metodo simile, vorrei condividerlo qui. Lo svantaggio di questa soluzione è che richiede la creazione di sottoclassi UINavigationController
. Sebbene l'utilizzo del Metodo Swizzling possa aiutare a evitarlo, non sono andato così lontano.
Quindi, il pulsante Indietro predefinito è effettivamente gestito da UINavigationBar
. Quando un utente tocca il pulsante Indietro, UINavigationBar
chiedi al suo delegato se deve apparire in alto UINavigationItem
chiamando navigationBar(_:shouldPop:)
. UINavigationController
lo implementa effettivamente, ma non dichiara pubblicamente di adottare UINavigationBarDelegate
(perché !?). Per intercettare questo evento, creare una sottoclasse di UINavigationController
, dichiararne la conformità UINavigationBarDelegate
e implementare navigationBar(_:shouldPop:)
. Restituisci true
se l'elemento in alto deve essere estratto. Ritorna false
se dovrebbe restare.
Ci sono due problemi. Il primo è che a un certo punto devi chiamare la UINavigationController
versione di navigationBar(_:shouldPop:)
. Ma UINavigationBarController
non lo dichiara pubblicamente conforme a UINavigationBarDelegate
, provare a chiamarlo risulterà in un errore in fase di compilazione. La soluzione che ho scelto è usare il runtime Objective-C per ottenere direttamente l'implementazione e chiamarla. Per favore fatemi sapere se qualcuno ha una soluzione migliore.
L'altro problema è che navigationBar(_:shouldPop:)
viene chiamato prima popViewController(animated:)
se l'utente tocca il pulsante Indietro. L'ordine si inverte se il controller di visualizzazione viene estratto chiamando popViewController(animated:)
. In questo caso, utilizzo un valore booleano per rilevare se popViewController(animated:)
viene chiamato prima, il navigationBar(_:shouldPop:)
che significa che l'utente ha toccato il pulsante Indietro.
Inoltre, creo un'estensione di UIViewController
per consentire al controller di navigazione di chiedere al controller di visualizzazione se deve essere visualizzato se l'utente tocca il pulsante Indietro. I controller di visualizzazione possono tornare false
ed eseguire tutte le azioni necessarie e chiamare popViewController(animated:)
più tardi.
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
}
}
E nella visualizzazione dei controller, implementare shouldBePopped(_:)
. Se non si implementa questo metodo, il comportamento predefinito sarà quello di far apparire il controller della vista non appena l'utente tocca il pulsante Indietro come al solito.
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
}
}
Puoi guardare la mia demo qui .