10

在 iOS 14+ 中,点击并按住backBarButtonItemUINavigationItem 将呈现完整的导航堆栈。然后用户可以弹出到堆栈中的任何点,而以前用户所能做的只是点击该项目以弹出堆栈中的一个项目。

是否可以禁用此功能?UIBarButtonItem 有一个名为 的新属性menu,但尽管在按住按钮时显示菜单,但它似乎为 nil。这让我相信这可能是无法改变的特殊行为,但也许我忽略了一些东西。

4

5 回答 5

13

它可以通过继承 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
  }
}
于 2020-09-19T01:14:30.437 回答
3

运行时调配是最终的解决方案。

这与Andrei Marincas子类和集合解决方案基本相同。

但是每次按下视图控制器时设置 backBarButtonItem 都会导致后退按钮上的过渡令人讨厌。

因此,我将默认设置器UIBarButtonItem.menu转换为无操作代码块,这对 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
    }
}
于 2021-06-11T07:13:54.277 回答
2

也从https://stackoverflow.com/a/64386494/95309复制答案。

如果您看到“空”菜单是因为您当前将 设置backButtonTitle为空字符串,或者将 设置backBarButtonItem为空标题以删除后退按钮标题,则应改为从 iOS 14 及更高版本设置backButtonDisplayModeminimal

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

于 2021-03-11T10:26:54.163 回答
1

在 didFinishLaunchingWithOptions 中调用 UIBarButtonItem.fix_classInit()。方法交换的目的是在菜单设置器中什么都不做。

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?) {
        }
}
于 2021-05-18T13:25:21.877 回答
-1

Andrei Marincas 的解决方案对我有用。然而,在每个根导航控制器上设置一个自定义 UIBarButtonItem 是很烦人的。在某些情况下,我发现设置自定义栏按钮项不适用于所有子类(可能如果子 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()

从这个答案中找到了使用嘶嘶声的想法:https ://stackoverflow.com/a/64713022/8817327

于 2021-04-15T08:03:16.987 回答