Search code examples
iosswiftswiftuiuinavigationbartabview

App Crash on dynamic Tab Bar items count SwiftUI


Having the following situation here. Basically I want to have dynamic amount of tab bar items depending on a situation. In the provided example below when the button is tapped it will add one more tab in the middle of the array (index 1) where previously stayed case .five. The problem I am having is that when the new .three tab shows and I click on it the app crashes with the following error:

"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."

The interesting part about it, is that it only happens on iOS 17+ and not on 16. The small POC I get the error on:

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")
            }
        }
    }
}

Solution

  • This was a bug several OSs ago, you should submit a report so it gets fixed again but as a workaround you can set an id for the TabView.

        TabView(selection: $selection) {
            ForEach(vm.tabs, id: \.rawValue) { tab in
                tab
                    .tag(tab)
                    .tabItem {
                        Text(tab.rawValue)
                    }
            }
        }.id(vm.tabs.count) // <——Here
    

    This redraws the entire TabView so it is very inefficient.