Search code examples
swiftswiftuiswiftui-navigationstack

NavigationStack does not display a view on UI after appending a new path


I am using SwiftUI NavigationStack as a router in my application. I have a ParentView as an initial view with a button. When I click a button I want to show a loading view and when API answer is received I want to move to a new View. The problem is that if the API answer is too fast, UI gets stuck on a LoadingView, even though a new navigation path is appended. If the answer takes some time - at least 1 second, everything works well.

I noticed that if I remove LoadingView and move this logic to ParentView (ParentView contains task), everything works well. I also noticed that this problem can be fixed by adding a delay. However, it has to be around one second not less. Do you have any idea what could cause this and how to fix it? Why delay helps and how to resolve it in a more proper way? Thanks in advance.

I know, it would be better if I used loading state of a same view and not a separate view but this is necessary because of my backend. It informs me about a next step in my view hierarchy based on some conditions - it needs a separate view for loading.

Code below is a very simplified version of an actual code in the project. However, it reproduces a mentioned bug exactly:

struct MainView: App {
    @StateObject var navigationPath = NavigationPath()

    var body: some Scene {
        NavigationStack(path: $navigationPath) {
            ParentView()
                .navigationDestination(for: String.self) { view in
                    // this part of code is not called when a new path is appended 
                    switch view {
                    case "parent":
                        ParentView()
                    case "loading":
                        LoadingView() // loading view
                    case "product":
                        ProductView() // target view
                    default:
                        EmptyView()
                    }
                }
        }
        .environmentObject(navigationPath)
    }
}

struct ParentView: View {
    @EnvironmentObject var navigationPath: NavigationPath
    var body: some View {
        Button {
            navigationPath.append("loading")
        } label: {
            Text("Go to loading screen")
        }
    }
}

struct LoadingView: View {
    @EnvironmentObject var navigationPath: NavigationPath

    var body: some View {
        ProgressView()
        .task {
            HERE we are doing some kind of API call - fetching data
            // If we uncomment next line - code works without any problems
            // try? await Task.sleep(until: .now + .seconds(1), clock: .continuous)
            navigationPath.append("product")
        }
    }
}

Solution

  • Sometimes it causes issues with SwiftUI when two items are pushed to the path in rapid succession. Then the second item gets "missed" and it will not be shown.

    In your case, when "loading" gets appended, then LoadingView gets shown and that immediately appends something new to the stack. This happens rapidly enough to cause that SwiftUI bug.

    Hence, if you add that delay, everything works nicely.

    A way around this would be to have the "loading" spinner be part of the product view itself. Then you just switch out the content of the product view rapidly (works fine) instead of pushing twice rapidly.

    Following this approach will also make things nicer to the user, because if they navigate back, they won't have to go through a "loading" view :)