iOS17+ SwiftUI动态TabBar切换新增标签时崩溃求助
问题场景
需要根据场景动态调整TabBar的项数量。示例代码中,点击按钮会在数组索引1位置(原.five标签处)插入或删除.three标签。但在iOS17及以上版本中,新增.three标签后点击它,App会崩溃。
错误信息
Layout requested for visible navigation bar, <SwiftUI.UIKitNavigationBar: 0x100710c50; baseClass = UINavigationBar; frame = (0 24; 1024 102); opaque = NO; autoresize = W; gestureRecognizers = <NSArray: 0x600000c63390>; layer = <CALayer: 0x60000026e5a0>> delegate=0x110016200, when the top item belongs to a different navigation bar. topItem = <UINavigationItem: 0x100712790> title='Hello' style=navigator leftItemsSupplementBackButton largeTitleDisplayMode=always, navigation bar = <SwiftUI.UIKitNavigationBar: 0x10951dc00; baseClass = UINavigationBar; frame = (0 24; 1024 102); opaque = NO; autoresize = W; gestureRecognizers = <NSArray: 0x600000c8b960>; layer = <CALayer: 0x60000028ad60>> delegate=0x10f052800, possibly from a client attempt to nest wrapped navigation controllers.
复现代码
class ContentViewModel: ObservableObject { @Published var tabs: [Tab] = [.one, .five] static var shared = ContentViewModel() } struct ContentView: View { @State var selection: Tab = .one @StateObject var vm = ContentViewModel.shared var body: some View { TabView(selection: $selection) { ForEach(vm.tabs, id: \.self) { tab in tab .tag(tab) .tabItem { Text(tab.rawValue) } } } } } enum Tab: String { case one = "One" case three = "Three" case five = "Five" } extension Tab: View { var body: some View { NavigationStack { switch self { case .one: Text("One") case .three: Text("Three") case .five: Button("Change") { if ContentViewModel.shared.tabs.count == 3 { ContentViewModel.shared.tabs.removeSubrange(1...1) } else { ContentViewModel.shared.tabs.insert(.three, at: 1) } } .navigationTitle("Hello") } } } }
解决方案
该问题是iOS17中TabView与NavigationStack交互的bug,根源是动态修改Tab数组时,SwiftUI未正确管理NavigationBar的关联关系,以下是几种修复方式:
方案1:给每个Tab的NavigationStack绑定唯一ID
通过给每个Tab对应的NavigationStack添加唯一ID,强制SwiftUI在Tab切换时重新创建对应实例,避免导航栏实例混乱:
extension Tab: View { var body: some View { NavigationStack { // 原有内容保持不变 switch self { case .one: Text("One") case .three: Text("Three") case .five: Button("Change") { if ContentViewModel.shared.tabs.count == 3 { ContentViewModel.shared.tabs.removeSubrange(1...1) } else { ContentViewModel.shared.tabs.insert(.three, at: 1) } } .navigationTitle("Hello") } } .id(self) // 添加该行绑定唯一ID } }
方案2:改用条件渲染控制Tab显示
放弃直接修改Tab数组,通过布尔值控制Tab的显示状态,避免动态数组引发的视图复用问题:
// 修改ViewModel class ContentViewModel: ObservableObject { @Published var showThreeTab = false static var shared = ContentViewModel() } // 修改ContentView struct ContentView: View { @State var selection: Tab = .one @StateObject var vm = ContentViewModel.shared var body: some View { TabView(selection: $selection) { Tab.one .tag(Tab.one) .tabItem { Text(Tab.one.rawValue) } if vm.showThreeTab { Tab.three .tag(Tab.three) .tabItem { Text(Tab.three.rawValue) } } Tab.five .tag(Tab.five) .tabItem { Text(Tab.five.rawValue) } } } } // 修改.five标签的按钮逻辑 extension Tab: View { var body: some View { NavigationStack { switch self { case .one: Text("One") case .three: Text("Three") case .five: Button("Change") { ContentViewModel.shared.showThreeTab.toggle() } .navigationTitle("Hello") } } } }
方案3:降级使用NavigationView
如果无需使用NavigationStack的新特性,可改用旧版NavigationView,该bug在旧组件中不会出现:
extension Tab: View { var body: some View { NavigationView { // 替换NavigationStack为NavigationView switch self { // 原有内容保持不变 case .one: Text("One") case .three: Text("Three") case .five: Button("Change") { if ContentViewModel.shared.tabs.count == 3 { ContentViewModel.shared.tabs.removeSubrange(1...1) } else { ContentViewModel.shared.tabs.insert(.three, at: 1) } } .navigationTitle("Hello") } } } }
说明
方案1对现有代码改动最小,是最推荐的修复方式,能直接解决iOS17+下的导航栏实例混乱问题。
内容的提问来源于stack exchange,提问作者Boyan Pavlov

