Можливо, трохи пізно, але я теж хотів такої ж поведінки і раніше. І рішення, з яким я працював, працює досить добре в одному з додатків, які зараз перебувають у App Store. Оскільки я не бачив, щоб хтось ходив подібним методом, я хотів би поділитися цим тут. Мінусом цього рішення є те, що воно вимагає підкласифікації UINavigationController
. Хоча використання методу Swizzling може допомогти уникнути цього, я не пішов так далеко.
Отже, кнопкою повернення за замовчуванням насправді керує UINavigationBar
. Коли користувач натискає кнопку "Назад", UINavigationBar
запитайте свого делегата, чи повинен він звертатися до вершини UINavigationItem
, зателефонувавши navigationBar(_:shouldPop:)
. UINavigationController
насправді це реалізовують, але він публічно не заявляє, що приймає UINavigationBarDelegate
(чому !?). Щоб перехопити цю подію, створіть підклас UINavigationController
, заявіть про його відповідність UINavigationBarDelegate
та реалізуйте navigationBar(_:shouldPop:)
. Повертайтеся, true
якщо верхній предмет повинен вискочити. Поверненняfalse
якщо воно залишиться.
Є дві проблеми. Перший полягає в тому, що ви повинні зателефонувати у UINavigationController
версію програми navigationBar(_:shouldPop:)
в якийсь момент. Але UINavigationBarController
публічно не заявляє про його відповідністьUINavigationBarDelegate
, спроба викликати його призведе до помилки часу компіляції. Рішення, з яким я пішов, - це використовувати час виконання Objective-C, щоб отримати реалізацію безпосередньо та викликати її. Будь ласка, дайте мені знати, чи є хтось кращого рішення.
Інша проблема полягає в тому, що navigationBar(_:shouldPop:)
викликається спочатку, після чого popViewController(animated:)
користувач натискає кнопку назад. Порядок змінюється, якщо контролер перегляду вискакує, зателефонувавши popViewController(animated:)
. У цьому випадку я використовую булевий сигнал, щоб виявити, чи popViewController(animated:)
викликається раніше, navigationBar(_:shouldPop:)
що означає, що користувач натиснув кнопку "назад".
Крім того, я роблю розширення, UIViewController
щоб дозволити контролеру навігації запитати контролер перегляду, чи слід його вискакувати, якщо користувач натискає кнопку назад. Контролери перегляду можуть повертатися false
та виконувати будь-які необхідні дії та дзвонити popViewController(animated:)
пізніше.
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
}
}
А у вас перегляд контролерів, реалізуйте shouldBePopped(_:)
. Якщо ви не реалізуєте цей метод, поведінка за замовчуванням полягає в тому, щоб відкрити контролер перегляду, як тільки користувач натискає кнопку назад, як звичайне.
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
}
}
Ви можете подивитися мою демонстрацію тут .