Có lẽ hơi muộn, nhưng trước đây tôi cũng muốn có những hành vi tương tự. Và giải pháp mà tôi đã sử dụng hoạt động khá tốt trong một trong những ứng dụng hiện có trên App Store. Vì mình chưa thấy ai đi theo phương pháp tương tự nên mình xin chia sẻ ở đây. Nhược điểm của giải pháp này là nó yêu cầu phân lớp con UINavigationController
. Mặc dù sử dụng Method Swizzling có thể giúp tránh điều đó, nhưng tôi đã không đi xa đến vậy.
Vì vậy, nút quay lại mặc định thực sự được quản lý bởi UINavigationBar
. Khi người dùng nhấn vào nút quay lại, UINavigationBar
hãy hỏi người đại diện của họ xem nó có bật lên trên cùng UINavigationItem
bằng cách gọi hay không navigationBar(_:shouldPop:)
. UINavigationController
thực sự thực hiện điều này, nhưng nó không tuyên bố công khai rằng nó thông qua UINavigationBarDelegate
(tại sao !?). Để chặn sự kiện này, hãy tạo một lớp con của UINavigationController
, khai báo sự phù hợp của nó UINavigationBarDelegate
và triển khai navigationBar(_:shouldPop:)
. Trở lại true
nếu mục hàng đầu sẽ được xuất hiện. Trả lại false
nếu nó nên ở lại.
Có hai vấn đề. Đầu tiên là bạn phải gọi UINavigationController
phiên bản của navigationBar(_:shouldPop:)
một lúc nào đó. Nhưng UINavigationBarController
không tuyên bố công khai nó tuân thủ UINavigationBarDelegate
, cố gắng gọi nó sẽ dẫn đến lỗi thời gian biên dịch. Giải pháp mà tôi đã đi là sử dụng Objective-C runtime để thực hiện trực tiếp và gọi nó. Vui lòng cho tôi biết nếu có ai có giải pháp tốt hơn.
Vấn đề khác là nó navigationBar(_:shouldPop:)
được gọi đầu tiên theo sau popViewController(animated:)
nếu người dùng nhấn vào nút quay lại. Thứ tự sẽ đảo ngược nếu bộ điều khiển chế độ xem được hiển thị bằng cách gọi popViewController(animated:)
. Trong trường hợp này, tôi sử dụng boolean để phát hiện xem popViewController(animated:)
có được gọi trước navigationBar(_:shouldPop:)
đó hay không, nghĩa là người dùng đã nhấn vào nút quay lại.
Ngoài ra, tôi thực hiện một phần mở rộng UIViewController
để cho phép bộ điều khiển điều hướng hỏi bộ điều khiển chế độ xem xem nó có nên được bật lên không nếu người dùng nhấn vào nút quay lại. Bộ điều khiển chế độ xem có thể quay lại false
và thực hiện bất kỳ hành động cần thiết nào và gọi popViewController(animated:)
sau.
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
}
}
Và trong bạn xem bộ điều khiển, thực hiện shouldBePopped(_:)
. Nếu bạn không triển khai phương pháp này, hành vi mặc định sẽ là bật bộ điều khiển chế độ xem ngay khi người dùng chạm vào nút quay lại giống như bình thường.
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
}
}
Bạn có thể xem bản demo của tôi ở đây .