是否可以在 iOS 14+ 中禁用后退导航菜单?
Is it possible to disable the back navigation menu in iOS 14+?
在 iOS 14+ 中,点击并按住 UINavigationItem 的 backBarButtonItem
将显示完整的导航堆栈。然后用户可以弹出到堆栈中的任何一点,而以前用户所能做的就是点击该项目以弹出堆栈中的一个项目。
是否可以禁用此功能? UIBarButtonItem 有一个名为 menu
的新 属性,但它似乎是 nil,尽管按住按钮时会显示菜单。这让我相信这可能是无法更改的特殊行为,但也许我忽略了一些东西。
可以通过子类化 UIBarButtonItem 来完成。在 UIBarButtonItem 上将菜单设置为 nil 不起作用,但您可以覆盖菜单 属性 并首先阻止设置它。
class BackBarButtonItem: UIBarButtonItem {
@available(iOS 14.0, *)
override var menu: UIMenu? {
set {
// Don't set the menu here
// super.menu = menu
}
get {
return super.menu
}
}
}
然后您可以按照自己喜欢的方式在视图控制器中配置后退按钮,但使用 BackBarButtonItem 而不是 UIBarButtonItem。
let backButton = BackBarButtonItem(title: "BACK", style: .plain, target: nil, action: nil)
navigationItem.backBarButtonItem = backButton
这是首选,因为您只在视图控制器的导航项中设置了一次 backBarButtonItem,然后无论它将推送什么视图控制器,推送的控制器都会自动在导航栏上显示后退按钮。如果使用 leftBarButtonItem 而不是 backBarButtonItem,则必须在将被推送的每个视图控制器上设置它。
编辑:
长按出现的后退导航菜单是一个属性的UIBarButtonItem。可以通过设置 navigationItem.backBarButtonItem 属性 来自定义视图控制器的后退按钮,这样我们就可以控制菜单。我看到这种方法的唯一问题是丢失了系统按钮具有的“后退”字符串的本地化(翻译)。
如果您希望禁用菜单成为默认行为,您可以在一个地方实现它,在符合 UINavigationControllerDelegate 的 UINavigationController 子类中:
class NavigationController: UINavigationController, UINavigationControllerDelegate {
init() {
super.init(rootViewController: ViewController())
delegate = self
}
func navigationController(_ navigationController: UINavigationController,
willShow viewController: UIViewController, animated: Bool) {
let backButton = BackBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
viewController.navigationItem.backBarButtonItem = backButton
}
}
也在此处复制 https://whosebug.com/a/64386494/95309 的答案。
如果您看到“空”菜单是因为您当前将 backButtonTitle
设置为空字符串,或者将 backBarButtonItem
设置为空标题以删除后面的内容按钮标题,您应该从 iOS 14 及以后将 backButtonDisplayMode
设置为 minimal
。
if #available(iOS 14.0, *) {
navigationItem.backButtonDisplayMode = .minimal
} else {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
https://developer.apple.com/documentation/uikit/uinavigationitem/3656350-backbuttondisplaymode
Andrei Marincas 的解决方案对我有用。然而,在每个根导航控制器上设置一个自定义的 UIBarButtonItem 是很麻烦的。在某些情况下,我发现设置自定义栏按钮项目不适用于所有 child 类(可能如果 child vc 对导航栏进行了一些修改故事板?)。所以我使用 swizzling 技术在每个 ViewDidLoad 上添加自定义后退栏按钮项目。
import UIKit
private let swizzling: (UIViewController.Type, Selector, Selector) -> Void = { forClass, originalSelector, swizzledSelector in
if let originalMethod = class_getInstanceMethod(forClass, originalSelector), let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) {
let didAddMethod = class_addMethod(forClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(forClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
extension UIViewController {
static func swizzle() {
let originalSelector1 = #selector(viewDidLoad)
let swizzledSelector1 = #selector(swizzled_viewDidLoad)
swizzling(UIViewController.self, originalSelector1, swizzledSelector1)
}
@objc open func swizzled_viewDidLoad() {
if let _ = navigationController {
let backButton = BackBarButtonItem(title: " ", style: .plain, target: nil, action: nil) // Set any title you'd like, I needed to show only the back icon.
navigationItem.backBarButtonItem = backButton
}
swizzled_viewDidLoad()
}
}
// From Andrei's answer
class BackBarButtonItem: UIBarButtonItem {
@available(iOS 14.0, *)
override var menu: UIMenu? {
set {
// Don't set the menu here
// super.menu = menu
}
get {
return super.menu
}
}
}
并在 application(_:didFinishLaunchingWithOptions:) 调用中
UIViewController.swizzle()
从这个答案中找到了使用 sizzling 的想法:
在 didFinishLaunchingWithOptions 中调用 UIBarButtonItem.fix_classInit()。
方法交换的目的是在菜单setter.
中什么都不做
func swizzlingClass(_ forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
guard let originalMethod = class_getInstanceMethod(forClass, originalSelector), let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) else {
return
}
if class_addMethod(forClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) {
class_replaceMethod(forClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
extension UIBarButtonItem {
public static func fix_classInit() {
if #available(iOS 14.0, *) {
swizzlingClass(UIBarButtonItem.self, originalSelector: #selector(setter: UIBarButtonItem.menu), swizzledSelector: #selector(fix_setMenu(menu:)))
}
}
@available(iOS 14.0, *)
@objc func fix_setMenu(menu: UIMenu?) {
}
}
运行时调配是最终的解决方案。
和Andrei Marincas's 的思路基本相同。
但是每次按下视图控制器时设置 backBarButtonItem 会导致后退按钮上出现烦人的过渡。
因此,我将 UIBarButtonItem.menu
的默认 setter 调整为一个什么都不做的代码块,这不会对 iOS 转换系统造成伤害。
只需复制此代码即可:
enum Runtime {
static func swizzle() {
if #available(iOS 14.0, *) {
exchange(
#selector(setter: UIBarButtonItem.menu),
with: #selector(setter: UIBarButtonItem.swizzledMenu),
in: UIBarButtonItem.self
)
}
}
private static func exchange(
_ selector1: Selector,
with selector2: Selector,
in cls: AnyClass
) {
guard
let method = class_getInstanceMethod(
cls,
selector1
),
let swizzled = class_getInstanceMethod(
cls,
selector2
)
else {
return
}
method_exchangeImplementations(method, swizzled)
}
}
@available(iOS 14.0, *)
private extension UIBarButtonItem {
@objc dynamic var swizzledMenu: UIMenu? {
get {
nil
}
set {
}
}
}
粘贴到任何地方。在你的 AppDelegate
:
中调用它
@main
class AppDelegate: UIResponder {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// ......
Runtime.swizzle()
return true
}
}
在 iOS 14+ 中,点击并按住 UINavigationItem 的 backBarButtonItem
将显示完整的导航堆栈。然后用户可以弹出到堆栈中的任何一点,而以前用户所能做的就是点击该项目以弹出堆栈中的一个项目。
是否可以禁用此功能? UIBarButtonItem 有一个名为 menu
的新 属性,但它似乎是 nil,尽管按住按钮时会显示菜单。这让我相信这可能是无法更改的特殊行为,但也许我忽略了一些东西。
可以通过子类化 UIBarButtonItem 来完成。在 UIBarButtonItem 上将菜单设置为 nil 不起作用,但您可以覆盖菜单 属性 并首先阻止设置它。
class BackBarButtonItem: UIBarButtonItem {
@available(iOS 14.0, *)
override var menu: UIMenu? {
set {
// Don't set the menu here
// super.menu = menu
}
get {
return super.menu
}
}
}
然后您可以按照自己喜欢的方式在视图控制器中配置后退按钮,但使用 BackBarButtonItem 而不是 UIBarButtonItem。
let backButton = BackBarButtonItem(title: "BACK", style: .plain, target: nil, action: nil)
navigationItem.backBarButtonItem = backButton
这是首选,因为您只在视图控制器的导航项中设置了一次 backBarButtonItem,然后无论它将推送什么视图控制器,推送的控制器都会自动在导航栏上显示后退按钮。如果使用 leftBarButtonItem 而不是 backBarButtonItem,则必须在将被推送的每个视图控制器上设置它。
编辑:
长按出现的后退导航菜单是一个属性的UIBarButtonItem。可以通过设置 navigationItem.backBarButtonItem 属性 来自定义视图控制器的后退按钮,这样我们就可以控制菜单。我看到这种方法的唯一问题是丢失了系统按钮具有的“后退”字符串的本地化(翻译)。
如果您希望禁用菜单成为默认行为,您可以在一个地方实现它,在符合 UINavigationControllerDelegate 的 UINavigationController 子类中:
class NavigationController: UINavigationController, UINavigationControllerDelegate {
init() {
super.init(rootViewController: ViewController())
delegate = self
}
func navigationController(_ navigationController: UINavigationController,
willShow viewController: UIViewController, animated: Bool) {
let backButton = BackBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
viewController.navigationItem.backBarButtonItem = backButton
}
}
也在此处复制 https://whosebug.com/a/64386494/95309 的答案。
如果您看到“空”菜单是因为您当前将 backButtonTitle
设置为空字符串,或者将 backBarButtonItem
设置为空标题以删除后面的内容按钮标题,您应该从 iOS 14 及以后将 backButtonDisplayMode
设置为 minimal
。
if #available(iOS 14.0, *) {
navigationItem.backButtonDisplayMode = .minimal
} else {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
https://developer.apple.com/documentation/uikit/uinavigationitem/3656350-backbuttondisplaymode
Andrei Marincas 的解决方案对我有用。然而,在每个根导航控制器上设置一个自定义的 UIBarButtonItem 是很麻烦的。在某些情况下,我发现设置自定义栏按钮项目不适用于所有 child 类(可能如果 child vc 对导航栏进行了一些修改故事板?)。所以我使用 swizzling 技术在每个 ViewDidLoad 上添加自定义后退栏按钮项目。
import UIKit
private let swizzling: (UIViewController.Type, Selector, Selector) -> Void = { forClass, originalSelector, swizzledSelector in
if let originalMethod = class_getInstanceMethod(forClass, originalSelector), let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) {
let didAddMethod = class_addMethod(forClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
if didAddMethod {
class_replaceMethod(forClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
}
extension UIViewController {
static func swizzle() {
let originalSelector1 = #selector(viewDidLoad)
let swizzledSelector1 = #selector(swizzled_viewDidLoad)
swizzling(UIViewController.self, originalSelector1, swizzledSelector1)
}
@objc open func swizzled_viewDidLoad() {
if let _ = navigationController {
let backButton = BackBarButtonItem(title: " ", style: .plain, target: nil, action: nil) // Set any title you'd like, I needed to show only the back icon.
navigationItem.backBarButtonItem = backButton
}
swizzled_viewDidLoad()
}
}
// From Andrei's answer
class BackBarButtonItem: UIBarButtonItem {
@available(iOS 14.0, *)
override var menu: UIMenu? {
set {
// Don't set the menu here
// super.menu = menu
}
get {
return super.menu
}
}
}
并在 application(_:didFinishLaunchingWithOptions:) 调用中
UIViewController.swizzle()
从这个答案中找到了使用 sizzling 的想法:
在 didFinishLaunchingWithOptions 中调用 UIBarButtonItem.fix_classInit()。 方法交换的目的是在菜单setter.
中什么都不做func swizzlingClass(_ forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
guard let originalMethod = class_getInstanceMethod(forClass, originalSelector), let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) else {
return
}
if class_addMethod(forClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) {
class_replaceMethod(forClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
} else {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
extension UIBarButtonItem {
public static func fix_classInit() {
if #available(iOS 14.0, *) {
swizzlingClass(UIBarButtonItem.self, originalSelector: #selector(setter: UIBarButtonItem.menu), swizzledSelector: #selector(fix_setMenu(menu:)))
}
}
@available(iOS 14.0, *)
@objc func fix_setMenu(menu: UIMenu?) {
}
}
运行时调配是最终的解决方案。
和Andrei Marincas's
但是每次按下视图控制器时设置 backBarButtonItem 会导致后退按钮上出现烦人的过渡。
因此,我将 UIBarButtonItem.menu
的默认 setter 调整为一个什么都不做的代码块,这不会对 iOS 转换系统造成伤害。
只需复制此代码即可:
enum Runtime {
static func swizzle() {
if #available(iOS 14.0, *) {
exchange(
#selector(setter: UIBarButtonItem.menu),
with: #selector(setter: UIBarButtonItem.swizzledMenu),
in: UIBarButtonItem.self
)
}
}
private static func exchange(
_ selector1: Selector,
with selector2: Selector,
in cls: AnyClass
) {
guard
let method = class_getInstanceMethod(
cls,
selector1
),
let swizzled = class_getInstanceMethod(
cls,
selector2
)
else {
return
}
method_exchangeImplementations(method, swizzled)
}
}
@available(iOS 14.0, *)
private extension UIBarButtonItem {
@objc dynamic var swizzledMenu: UIMenu? {
get {
nil
}
set {
}
}
}
粘贴到任何地方。在你的 AppDelegate
:
@main
class AppDelegate: UIResponder {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// ......
Runtime.swizzle()
return true
}
}