也许有点太晚了,但我以前也想要同样的行为。我采用的解决方案在 App Store 目前的一款应用中运行良好。由于我还没有看到有人使用类似的方法,所以我想在这里分享一下。这个解决方案的缺点是它需要子类化UINavigationController。虽然使用Method Swizzling 可能有助于避免这种情况,但我并没有走那么远。
所以,默认的后退按钮实际上是由UINavigationBar 管理的。当用户点击返回按钮时,UINavigationBar 会通过调用 navigationBar(_:shouldPop:) 询问其代表是否应该弹出顶部的 UINavigationItem。 UINavigationController 实际上实现了这一点,但它没有公开声明它采用UINavigationBarDelegate(为什么!?)。要拦截此事件,请创建UINavigationController 的子类,声明其符合UINavigationBarDelegate 并实现navigationBar(_:shouldPop:)。如果应该弹出顶部项目,则返回 true。如果应该保留,请返回 false。
有两个问题。首先是您必须在某个时候调用navigationBar(_:shouldPop:) 的UINavigationController 版本。但是UINavigationBarController 并没有公开声明它符合UINavigationBarDelegate,试图调用它会导致编译时错误。我采用的解决方案是使用 Objective-C 运行时直接获取实现并调用它。如果有人有更好的解决方案,请告诉我。
另一个问题是如果用户点击后退按钮,navigationBar(_:shouldPop:) 会首先被调用,然后是popViewController(animated:)。如果通过调用popViewController(animated:) 弹出视图控制器,则顺序相反。在这种情况下,我使用布尔值来检测是否在navigationBar(_:shouldPop:) 之前调用了popViewController(animated:),这意味着用户已经点击了返回按钮。
另外,我扩展了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
}
}
你可以看看我的演示here。