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

12

iOS14及以上版本中,在UINavigationItem的backBarButtonItem上轻按并长按,将呈现完整的导航堆栈。然后用户可以弹出堆栈中的任何一个点,而以前用户只能轻按此项以弹出堆栈中的一项。

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


https://dev59.com/_mAg5IYBdhLWcg3wM4r_#49267846 看起来证实了我的怀疑,这是由私有API控制的,但也许还有办法... - mmd1080
你实现了吗? - Jalil
你看/尝试过 https://dev59.com/Arvoa4cB1Zd3GeqP2V9f 吗?我刚把这段代码添加到我的项目中,它按照我想要的方式工作了... - Zonker.in.Geneva
5个回答

15

可以通过子类化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属性,可以自定义视图控制器的返回按钮,并控制菜单。我所看到的唯一问题是失去了系统按钮的本地化(翻译)“Back”字符串。

如果您希望禁用菜单成为默认行为,则可以在UINavigationController子类中实现此功能,并符合UINavigationControllerDelegate:

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
@matt 有办法在导航控制器栈中的每个视图控制器的navigationItem上设置backBarButtonItem,例如通过实现UINavigationControllerDelegate的navigationController:didShow:委托方法来实现。您不必在每个视图控制器中都这样做。但是依赖leftBarButtonItem来弹出可能被推送的每个视图控制器是有风险的。如果您感兴趣,可以在此处找到更完整的答案:https://developer.apple.com/forums/thread/653913?answerId=634040022#634040022 - Andrei Marincas
@matt 是的,您需要在每个将被推送的UIViewController对象上设置自定义返回按钮,以便对菜单进行控制。但是,在编程方面,您可以在一个地方实现它。我看到的唯一问题是您会失去“返回”字符串本地化。我没有看到其他禁用菜单的正统方法。系统返回按钮不是公共的。 - Andrei Marincas
这个功能真是没用,我讨厌它给用户带来的空洞和困惑。 - Yaroslav Dukal
这个3D Touch在应用程序中可以完全禁用吗? - Yaroslav Dukal

5

运行时方法交换是最终解决方案。

这基本上与Andrei Marincas子类和设置解决方案相同。

但每次推送一个视图控制器时设置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
    }
}

2

以下是https://stackoverflow.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


2
它不会禁用,但可以防止其完全为空,同时保持清洁的返回按钮。 - Medhi

1
在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?) {
        }
}

2
通常,如果回答中包括代码的意图以及为什么这样做可以解决问题而不引入其他问题,那么回答会更有帮助。 - DCCoder

-1
Andrei Marincas的解决方案对我很有用。然而,在每个根导航控制器上设置自定义UIBarButtonItem是很繁琐的。在某些情况下,我发现为所有子类设置自定义工具栏按钮项并不起作用(可能是因为子vc从storyboard中对Navbar进行了一些修改?)。因此,我使用了交换技术来在每个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://stackoverflow.com/a/64713022/8817327


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接