如何通过代码为Navigation Controller的所有ViewController添加右侧Bar Button Item
问题描述
在Scene Delegate中创建UINavigationController后,尝试直接给导航控制器设置右侧Bar Button Item,但没有任何ViewController显示该按钮:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) let viewController = IntroViewController() let navigation = UINavigationController(rootViewController: viewController) // 这段代码无效,导航栏显示的是栈顶VC的navigationItem navigation.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Test", style: .done, target: nil, action: nil) navigation.navigationItem.rightBarButtonItem?.tintColor = .red let standardAppearance = UINavigationBarAppearance() standardAppearance.backgroundColor = .black navigation.navigationBar.standardAppearance = standardAppearance navigation.navigationBar.scrollEdgeAppearance = standardAppearance window.rootViewController = navigation self.window = window window.makeKeyAndVisible() }
只有在单个ViewController的viewDidLoad中设置,该VC才会显示按钮:
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Exit", style: .plain, target: self, action: #selector(exitPressed))
原因分析
UINavigationController自身的navigationItem不会被导航栏显示,导航栏始终展示当前导航栈顶部ViewController的navigationItem,所以直接给导航控制器设置按钮是无效的。
解决方案
方案一:创建UIViewController基类(推荐)
定义一个基类,所有需要显示右侧按钮的ViewController都继承这个基类:
class BaseViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // 统一添加右侧按钮 let rightButton = UIBarButtonItem(title: "Test", style: .done, target: self, action: #selector(rightButtonTapped)) rightButton.tintColor = .red navigationItem.rightBarButtonItem = rightButton } @objc func rightButtonTapped() { // 通用点击逻辑,子类可重写 print("右侧按钮被点击") } }
修改业务ViewController的继承关系:
class IntroViewController: BaseViewController { // 若需自定义按钮,可重写viewDidLoad或rightButtonTapped方法 }
优点:逻辑清晰、易维护,子类可灵活扩展,是最安全的方案。
方案二:利用UINavigationController代理
通过导航控制器的代理方法,在ViewController即将显示时统一设置按钮:
- 让SceneDelegate遵守
UINavigationControllerDelegate - 在SceneDelegate中添加代码:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) let viewController = IntroViewController() let navigation = UINavigationController(rootViewController: viewController) // 设置代理 navigation.delegate = self let standardAppearance = UINavigationBarAppearance() standardAppearance.backgroundColor = .black navigation.navigationBar.standardAppearance = standardAppearance navigation.navigationBar.scrollEdgeAppearance = standardAppearance window.rootViewController = navigation self.window = window window.makeKeyAndVisible() } // MARK: - UINavigationControllerDelegate func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { // 避免重复设置按钮 guard viewController.navigationItem.rightBarButtonItem == nil else { return } let rightButton = UIBarButtonItem(title: "Test", style: .done, target: viewController, action: #selector(viewController.rightButtonTapped)) rightButton.tintColor = .red viewController.navigationItem.rightBarButtonItem = rightButton }
- 给UIViewController添加扩展实现点击方法:
extension UIViewController { @objc func rightButtonTapped() { print("右侧按钮被点击") // 子类可重写此方法实现自定义逻辑 } }
优点:无需修改现有ViewController的继承关系,适合已有项目。
方案三:方法Swizzling(谨慎使用)
通过运行时替换viewDidLoad方法,自动给所有ViewController添加按钮:
extension UIViewController { static func swizzleViewDidLoad() { let originalSelector = #selector(viewDidLoad) let swizzledSelector = #selector(swizzledViewDidLoad) guard let originalMethod = class_getInstanceMethod(self, originalSelector), let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) else { return } method_exchangeImplementations(originalMethod, swizzledMethod) } @objc func swizzledViewDidLoad() { // 先执行原viewDidLoad逻辑 self.swizzledViewDidLoad() // 排除UINavigationController本身,避免重复设置 guard self is UINavigationController == false else { return } if navigationItem.rightBarButtonItem == nil { let rightButton = UIBarButtonItem(title: "Test", style: .done, target: self, action: #selector(rightButtonTapped)) rightButton.tintColor = .red navigationItem.rightBarButtonItem = rightButton } } @objc func rightButtonTapped() { print("右侧按钮被点击") } }
在SceneDelegate的scene方法开头调用Swizzle:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // 执行Swizzle UIViewController.swizzleViewDidLoad() // 后续原有代码... }
注意:Swizzling属于运行时黑魔法,可能与第三方库冲突,调试难度大,仅在必要时使用。
内容的提问来源于stack exchange,提问作者Shiv Nik
相关产品推荐
相关产品推荐

