Może jest już trochę za późno, ale chciałem też wcześniej takiego zachowania. Rozwiązanie, które wybrałem, działa całkiem dobrze w jednej z aplikacji obecnie dostępnych w App Store. Ponieważ nie widziałem nikogo, kto stosuje podobną metodę, chciałbym się nią tutaj podzielić. Wadą tego rozwiązania jest to, że wymaga podklasy UINavigationController
. Chociaż użycie Method Swizzling może pomóc w uniknięciu tego, nie posunąłem się tak daleko.
Tak więc domyślny przycisk Wstecz jest faktycznie zarządzany przez UINavigationBar
. Gdy użytkownik naciśnie przycisk Wstecz, UINavigationBar
zapytaj delegata, czy powinien otworzyć górną część UINavigationItem
, dzwoniąc navigationBar(_:shouldPop:)
. UINavigationController
faktycznie implementuje to, ale nie deklaruje publicznie, że przyjmuje UINavigationBarDelegate
(dlaczego !?). Aby przechwycić to zdarzenie, utwórz podklasę UINavigationController
, zadeklaruj jej zgodność UINavigationBarDelegate
i zaimplementuj navigationBar(_:shouldPop:)
. Zwróć, true
jeśli górny element powinien zostać usunięty. Wróć, false
jeśli powinno zostać.
Są dwa problemy. Po pierwsze, musisz kiedyś wywołać UINavigationController
wersję programu navigationBar(_:shouldPop:)
. Ale UINavigationBarController
nie deklaruje publicznie, że jest zgodny z UINavigationBarDelegate
, próba wywołania spowoduje błąd czasu kompilacji. Rozwiązaniem, które wybrałem, jest użycie środowiska uruchomieniowego Objective-C, aby bezpośrednio pobrać implementację i wywołać ją. Daj mi znać, jeśli ktoś ma lepsze rozwiązanie.
Innym problemem jest to, że navigationBar(_:shouldPop:)
najpierw wywoływana jest następująca popViewController(animated:)
po dotknięciu przycisku Wstecz. Kolejność jest odwracana, jeśli kontroler widoku jest otwierany przez wywołanie popViewController(animated:)
. W tym przypadku używam wartości logicznej, aby wykryć, czy popViewController(animated:)
została wywołana przed, navigationBar(_:shouldPop:)
co oznacza, że użytkownik nacisnął przycisk Wstecz.
Robię również rozszerzenie, UIViewController
aby umożliwić kontrolerowi nawigacji pytanie kontrolera widoku, czy należy go otworzyć, jeśli użytkownik naciśnie przycisk Wstecz. Kontrolery widoku mogą wrócić false
i wykonać niezbędne czynności, a popViewController(animated:)
później zadzwonić .
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
}
}
A widząc kontrolery, implementuj shouldBePopped(_:)
. Jeśli nie zaimplementujesz tej metody, domyślnym zachowaniem będzie wyskakiwanie kontrolera widoku, gdy tylko użytkownik naciśnie przycisk Wstecz, tak jak zwykle.
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
}
}
Możesz obejrzeć moje demo tutaj .