是否可以在 iOS 14+ 中禁用后退导航菜单?

Posted

技术标签:

【中文标题】是否可以在 iOS 14+ 中禁用后退导航菜单?【英文标题】:Is it possible to disable the back navigation menu in iOS 14+? 【发布时间】:2020-07-13 20:13:09 【问题描述】:

ios 14+ 中,点击并按住 UINavigationItem 的 backBarButtonItem 将显示完整的导航堆栈。然后用户可以弹出到堆栈中的任何点,而以前用户所能做的只是点击该项目以弹出堆栈中的一项。

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

【问题讨论】:

***.com/a/49267846/4759154 似乎证实了我的怀疑,即这是由私有 API 控制的,但也许有办法...... 你做到了吗? 你看到/尝试***.com/questions/62158125/…了吗?我刚刚将此代码添加到我的项目中,它可以按我的意愿工作.... 【参考方案1】:

可以通过继承 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
  

【讨论】:

聪明,但你也必须在每个视图控制器上都这样做。 @matt 有一些方法可以在堆栈中的每个视图控制器的 navigationItem 上设置一个 backBarButtonItem,例如通过实现 UINavigationControllerDelegate 的 navigationController:didShow: 委托方法。您不必在每个视图控制器中都这样做。但是要依靠 leftBarButtonItem 来为每个可能被推送的视图控制器执行 popViewController,这是有风险的。如果您有兴趣,这里有更完整的答案:developer.apple.com/forums/thread/… @matt 是的,您必须在每个将被推送的 UIViewController 对象上设置一个自定义后退按钮,以便控制菜单。但是以编程方式,您可以在一个地方实现它。我看到的唯一问题是您丢失了“Back”字符串本地化。我没有看到任何其他正统的禁用菜单的方法。系统后退按钮不公开。 虽然这是一个无用的功能,但我讨厌它给用户带来混乱的泡沫 这个 3D touch 可以在应用中完全禁用吗?【参考方案2】:

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

和Andrei Marincas的subclass and set solution的思路基本一样。

但是每次按下视图控制器时设置 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
    

【讨论】:

【参考方案3】:

这里也复制https://***.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

【讨论】:

它不会禁用,但可以防止它全部为空,同时保持干净的后退按钮。【参考方案4】:

在 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?) 
        

【讨论】:

一般来说,如果答案包含对代码的用途的解释,以及为什么在不介绍其他人的情况下解决问题的原因,答案会更有帮助。【参考方案5】:

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()

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

【讨论】:

以上是关于是否可以在 iOS 14+ 中禁用后退导航菜单?的主要内容,如果未能解决你的问题,请参考以下文章

iOS导航栏:将左栏按钮从菜单更改为后退

滑动时禁用网页导航(后退和前进)

如何检测由滑动触发的后退导航并在iOS上将其停止?

禁用后退按钮导航操作

在反应导航中禁用后退按钮

如何在 iOS 7 上的 UINavigationController 中禁用向后滑动手势