iOS中使用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

