You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS中使用MDCBottomNavigationBar切换视图控制器报错求助

解决MDCBottomNavigationBar切换视图控制器时的崩溃问题

首先,我得指出你当前实现里的一个核心问题:你同时使用了UITabBarController的原生底部栏和MDCBottomNavigationBar,这不仅造成了冗余,更会导致控制器层级混乱,进而引发AppBar的父控制器报错。你遇到的NSInternalInconsistencyException正是因为appBar.headerViewController的父控制器关系在子VC切换时出现了冲突。

下面给你两种可行的解决方案,你可以根据需求选择:

方案一:用MDCBottomNavigationBar配合普通UIViewController管理切换(推荐)

放弃继承UITabBarController,改用普通UIViewController手动管理子VC的添加/移除,这样能完全掌控控制器层级,避免冲突。

重构后的代码如下:

import Foundation
import MaterialComponents
import UIKit

class ICEBottomNavController: UIViewController, MDCBottomNavigationBarDelegate {
    let appBar = MDCAppBar()
    var colorScheme = MDCSemanticColorScheme()
    let bottomNavBar = MDCBottomNavigationBar()
    
    // 存储所有子视图控制器
    private var childViewControllers: [UIViewController] = []
    // 当前显示的子控制器
    private var currentChildVC: UIViewController?
    
    init() {
        super.init(nibName: nil, bundle: nil)
        initCommon()
    }
    
    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initCommon()
    }
    
    func initCommon() {
        self.title = "Bottom Navigation (Swift)"
        
        // 初始化三个子VC
        let statusVC = ICEChildBottomBarViewController(title: "Status", color: .orange)
        let eventsVC = ICEChildBottomBarViewController(title: "Events", color: .blue)
        let contactsVC = ICEChildBottomBarViewController(title: "Contacts", color: .cyan)
        
        childViewControllers = [statusVC, eventsVC, contactsVC]
        
        // 正确添加AppBar的headerViewController到当前控制器
        addChild(appBar.headerViewController)
        appBar.headerViewController.didMove(toParent: self)
        
        // 配置AppBar样式
        let navBarColor = UIColor(white: 0.2, alpha: 1)
        appBar.headerViewController.headerView.backgroundColor = navBarColor
        appBar.navigationBar.tintColor = .white
        appBar.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
        
        // 初始化底部导航栏
        commonBottomNavigationTypicalUseSwiftExampleInit()
        
        // 默认显示第一个标签页
        switchToChildVC(at: 0)
    }
    
    // 底部导航栏点击事件处理
    func bottomNavigationBar(_ bottomNavigationBar: MDCBottomNavigationBar, didSelect item: UITabBarItem) {
        print("did select item \(item.tag)")
        switchToChildVC(at: item.tag)
    }
    
    // 核心:切换子VC的方法,确保控制器层级正确
    private func switchToChildVC(at index: Int) {
        guard index < childViewControllers.count else { return }
        
        // 移除当前显示的子VC
        currentChildVC?.willMove(toParent: nil)
        currentChildVC?.view.removeFromSuperview()
        currentChildVC?.removeFromParent()
        
        // 添加新的子VC
        let newChildVC = childViewControllers[index]
        addChild(newChildVC)
        
        // 调整子VC的视图范围,避开AppBar和底部导航栏
        let headerHeight = appBar.headerViewController.headerView.bounds.height
        let bottomBarHeight = bottomNavBar.bounds.height
        newChildVC.view.frame = CGRect(
            x: 0,
            y: headerHeight,
            width: view.bounds.width,
            height: view.bounds.height - headerHeight - bottomBarHeight
        )
        
        view.insertSubview(newChildVC.view, belowSubview: bottomNavBar)
        newChildVC.didMove(toParent: self)
        
        currentChildVC = newChildVC
        
        // 更新AppBar标题
        appBar.navigationBar.title = newChildVC.title
    }
    
    func commonBottomNavigationTypicalUseSwiftExampleInit() {
        view.backgroundColor = .lightGray
        view.addSubview(bottomNavBar)
        
        // 配置底部导航栏样式
        bottomNavBar.titleVisibility = .always
        bottomNavBar.alignment = .centered
        
        // 添加标签项
        let tabBarItem1 = UITabBarItem(title: "Status", image: nil, tag: 0)
        let tabBarItem2 = UITabBarItem(title: "Events", image: nil, tag: 1)
        let tabBarItem3 = UITabBarItem(title: "Contacts", image: nil, tag: 2)
        
        bottomNavBar.items = [tabBarItem1, tabBarItem2, tabBarItem3]
        bottomNavBar.selectedItem = tabBarItem1
        bottomNavBar.delegate = self
    }
    
    // 布局底部导航栏,适配屏幕旋转和安全区域
    func layoutBottomNavBar() {
        let size = bottomNavBar.sizeThatFits(view.bounds.size)
        let bottomNavBarFrame = CGRect(
            x: 0,
            y: view.bounds.height - size.height,
            width: size.width,
            height: size.height
        )
        bottomNavBar.frame = bottomNavBarFrame
        
        // 同步调整当前子VC的视图范围
        currentChildVC?.view.frame = CGRect(
            x: 0,
            y: appBar.headerViewController.headerView.bounds.height,
            width: view.bounds.width,
            height: view.bounds.height - appBar.headerViewController.headerView.bounds.height - size.height
        )
    }
    
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        layoutBottomNavBar()
    }
    
    #if swift(>=3.2)
    @available(iOS 11, *)
    override func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
        layoutBottomNavBar()
    }
    #endif
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        appBar.addSubviewsToParent()
        // 应用主题到底部导航栏
        MDCBottomNavigationBarColorThemer.applySemanticColorScheme(colorScheme, toBottomNavigation: bottomNavBar)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(true, animated: animated)
    }
}

// MARK: Catalog by convention
extension ICEBottomNavController {
    class func catalogBreadcrumbs() -> [String] {
        return ["Bottom Navigation", "Bottom Navigation (Swift)"]
    }
    
    class func catalogIsPrimaryDemo() -> Bool {
        return false
    }
    
    func catalogShouldHideNavigation() -> Bool {
        return true
    }
}

方案说明:

  • 不再依赖UITabBarController,完全通过MDCBottomNavigationBar和手动代码管理视图切换
  • 专门的switchToChildVC(at:)方法确保子VC的添加/移除流程符合iOS控制器生命周期规范
  • 自动调整子VC的视图范围,避开AppBar和底部导航栏的区域
  • 切换标签时同步更新AppBar的标题,保持界面一致性

方案二:继续使用UITabBarController,美化原生TabBar

如果你坚持想用UITabBarController,可以移除手动添加的MDCBottomNavigationBar,改用MDC的主题工具美化原生TabBar:

import Foundation
import MaterialComponents
import UIKit

class ICEBottomNavController: UITabBarController {
    let appBar = MDCAppBar()
    var colorScheme = MDCSemanticColorScheme()
    
    init() {
        super.init(nibName: nil, bundle: nil)
        initCommon()
    }
    
    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initCommon()
    }
    
    func initCommon() {
        self.title = "Bottom Navigation (Swift)"
        
        // 初始化子VC并包裹导航控制器
        let statusVC = ICEChildBottomBarViewController(title: "Status", color: .orange)
        let eventsVC = ICEChildBottomBarViewController(title: "Events", color: .blue)
        let contactsVC = ICEChildBottomBarViewController(title: "Contacts", color: .cyan)
        
        let statusNav = UINavigationController(rootViewController: statusVC)
        let eventsNav = UINavigationController(rootViewController: eventsVC)
        let contactsNav = UINavigationController(rootViewController: contactsVC)
        
        self.viewControllers = [statusNav, eventsNav, contactsNav]
        
        // 配置原生TabBar样式
        tabBar.titleVisibility = .always
        tabBar.alignment = .centered
        
        // 用MDC主题美化原生TabBar
        MDCTabBarColorThemer.applySemanticColorScheme(colorScheme, toTabBar: tabBar)
        
        // 配置AppBar
        addChild(appBar.headerViewController)
        appBar.headerViewController.didMove(toParent: self)
        
        let navBarColor = UIColor(white: 0.2, alpha: 1)
        appBar.headerViewController.headerView.backgroundColor = navBarColor
        appBar.navigationBar.tintColor = .white
        appBar.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        appBar.addSubviewsToParent()
        self.selectedIndex = 0
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(true, animated: animated)
    }
    
    // 监听TabBar切换,更新AppBar标题
    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        appBar.navigationBar.title = item.title
    }
}

// MARK: Catalog by convention
extension ICEBottomNavController {
    class func catalogBreadcrumbs() -> [String] {
        return ["Bottom Navigation", "Bottom Navigation (Swift)"]
    }
    
    class func catalogIsPrimaryDemo() -> Bool {
        return false
    }
    
    func catalogShouldHideNavigation() -> Bool {
        return true
    }
}

方案说明:

  • 利用UITabBarController的原生切换逻辑,避免手动管理的复杂度
  • 用MDCTabBarColorThemer将原生TabBar替换为Material Design风格
  • 每个子VC包裹UINavigationController,确保导航层级正确,解决AppBar的父控制器问题

关于报错的补充说明

你之前遇到的NSInternalInconsistencyException,本质是因为appBar.headerViewController被添加到了UITabBarController中,但Material Design的AppBar组件期望它属于当前活跃的子VC,而非TabBarController本身。当你切换子VC时,这种父控制器关系的冲突触发了内部一致性检查,导致崩溃。注释掉self.viewControllers = [...]后,没有子VC切换,所以AppBar的父关系不会被破坏,因此不会崩溃,但也无法切换视图。

官方文档参考

如果你想深入学习Material Components iOS的底部导航组件,可以查看官方文档中关于Bottom Navigation的章节,里面有完整的示例代码,涵盖了组件集成、主题定制、与其他组件(如AppBar)配合的最佳实践,能帮你更清晰地理解控制器层级和组件交互逻辑。

内容的提问来源于stack exchange,提问作者Alex Corn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 10:07:43