Search code examples
swiftuiswiftui-navigationlinkswiftui-navigationstackios18swiftui-environment

Navigation Issue in iOS 18: Duplication of Navigation Trigger When Using @Environment(\.dismiss) in SwiftUI


I’m encountering an issue with SwiftUI navigation in iOS 18, where navigating to a DetailView causes unexpected duplication of navigation behavior when @Environment(.dismiss) is used.

Code Example: Here’s a simplified version of the code:

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink("Go to Detail View", destination: DetailView())
                .padding()
        }
    }
}

struct DetailView: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack {
            let _ = print("DetailView") // This print statement is triggered twice in iOS 18
        }
    }
}

Issue:

  • In iOS 18, when @Environment(.dismiss) is used in DetailView, the print("DetailView") statement is triggered twice.
  • The same code works correctly in iOS 17 and earlier, where the print statement is only triggered once, as expected.
  • However, when I remove @Environment(.dismiss) from DetailView, the code works as intended in iOS 18, with the print statement being triggered only once and no duplication of navigation behavior.

Alternative Approach with .navigationDestination(for:): I also tested using .navigationDestination(for:) to handle navigation:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Mint", value: Color.mint)
                NavigationLink("Pink", value: Color.pink)
                NavigationLink("Teal", value: Color.teal)
            }
            .navigationDestination(for: Color.self) { _ in
                DetailView()
            }
            .navigationTitle("Colors")
        }
    }
}

Even with this alternative approach, the issue persists in iOS 18, where the print statement is triggered twice.

What I've Tried:

  • I’ve confirmed that removing @Environment(.dismiss) solves the issue, and the print statement is triggered only once, and the navigation works as expected in iOS 18 without duplication.
  • The issue only occurs when @Environment(.dismiss) is in use, which seems to be tied to the navigation stack behavior. The code works correctly in iOS 17 and below, where the print statement is only called once.

Expected Behavior:

  • I expect the print("DetailView") statement to be called once when navigating to DetailView, and that the navigation happens only once without duplication. The presence of @Environment(.dismiss) should not cause the navigation to be triggered multiple times.

Questions:

  • Is this a known issue with iOS 18 and SwiftUI navigation? Specifically, is there a new behavior that interacts differently with @Environment(.dismiss)?
  • Has anyone else encountered this problem, and if so, what’s the recommended way to handle it in iOS 18?
  • Is there a workaround to ensure that the navigation doesn’t trigger more than once when using @Environment(.dismiss) in iOS 18?

Any help or insights would be greatly appreciated!


Solution

  • The body of a view may be fetched any number of times and the print statement will print its output every time this happens. You shouldn't rely on the body only being fetched once.

    If you want to perform something when the view is first shown, use .onAppear, or .task:

    VStack {
    }
    .onAppear {
        print("DetailView")
    }
    

    EDIT Following from your comment, if the issue is with a child view that shouldn't be shown until the parent is really showing, try setting a flag in .onAppear and then making the content conditional on this flag. The flag can be reset again in .onDisappear:

    @State private var isShowing = false
    
    VStack {
        if isShowing {
            // The problem view can be shown here
        }
    }
    .onAppear { isShowing = true }
    .onDisappear { isShowing = false }