Search code examples
iosswiftuiswiftui-navigationviewswiftui-tabview

Change Content of TabView Dynamically Cause Page to Become Blank


When I try to change the content of a TabView in runtime, the app crashes or show a blank screen. Platform: Xcode 15.0 beta 6 (15A5219j) / iOS 17.0 beta 5 (21A5303d)

Below is the minimal code that can reproduce this issue. Note that this code will run normally as expected on iOS 16.

import SwiftUI

class Coordinator: ObservableObject {
    @Published var open = false
}

struct TabbedView: View {
    @StateObject var coordinator = Coordinator()
    
    var body: some View {
        TabView {
            if coordinator.open {
                Text("Hello World!")
                    .tabItem {
                        Label("Feature", systemImage: "star")
                    }
            }
            
            SettingsView()
                .tabItem {
                    Label("Settings", systemImage: "gear")
                }
        }
        .environmentObject(coordinator)
    }
}

struct SettingsView: View {
    @EnvironmentObject var coordinator: Coordinator
    
    var body: some View {
        NavigationStack {
            List {
                Button("Toggle") {
                    coordinator.open.toggle()
                }
            }
            .navigationTitle("Settings")
        }
    }
}

The app has 2 sections: A blank feature section and a settings section. In the settings section, there is a button that can control whether the feature section should appear. When I tap the button for the first time, the feature section appears normally. However, when I tap the button for the second time, the feature section disappears, but all content within settings section disappears, the page just become blank.

The NavigationStack is crucial for the bug to appear. Did I do something wrong here, or is it a bug of SwiftUI?


Solution

  • Your approach of having a NavigationStack for every tab is the correct approach. specifies that the tabs should not share navigation in the Human Interface Guidelines.

    You can solve your issue by "reserving" the spot with EmptyView.

    struct TabbedView: View {
        @StateObject var coordinator = Coordinator()
        
        var body: some View {
            TabView {
                if coordinator.open {
                    Text("Hello World!")
                        .tabItem {
                            Label("Feature", systemImage: "star")
                        }
                } else { //"reserve" spot
                    EmptyView()
                }
                
                SettingsView()
                    .tabItem {
                        Label("Settings", systemImage: "gear")
                    }
            }
            .environmentObject(coordinator)
        }
    }
    

    You should submit this as a bug though, hopefully apple will eventually fix these types of issues.

    This is just a workaround, it may or may not work in certain versions.

    A more reliable way is to redraw everything but it is very inefficient.

    struct TabbedView: View {
        @StateObject var coordinator = Coordinator()
        
        var body: some View {
            TabView {
                if coordinator.open {
                    Text("Hello World!")
                        .tabItem {
                            Label("Feature", systemImage: "star")
                        }
                }
                
                SettingsView()
                    .tabItem {
                        Label("Settings", systemImage: "gear")
                    }
            }
            .id(coordinator.open) //Set an id
            .environmentObject(coordinator)
        }
    }