Search code examples
iosswiftswiftui

SwiftUI NavigationLink Freezes when View contains `dismiss` Environment value


When tapping a NavigationLink within a NavigationStack, my app freezes and is unrecoverable:

import SwiftUI

@main
struct State_BugApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationSplitView {
                List {
                    Section {
                        Text("Home")
                    }
                }.listStyle(.insetGrouped)
            } detail: {
                NavigationStack {
                    MainView()
                }
            }
        }
    }
}

struct MainView: View {
    @Environment(\.dismiss) private var dismiss
    @State var action: String = "Open child"
    
    var body: some View {
        List {
            NavigationLink {
                listDestination()
            } label: {
                Text(action)
            }
        }
    }

    @ViewBuilder func listDestination() -> some View {
        ChildView(action: { value in foo() })
    }

    func foo() {
        self.action = "Updated action"
    }
}

struct ChildView: View {
    @StateObject var viewModel: ChildViewModel = ChildViewModel()
    var action: (String) -> Void

    init(action: @escaping (String) -> Void) {
        self.action = action
    }

    var body: some View {
        List {
            Text("Text")
        }
    }
}

class ChildViewModel: ObservableObject {
    var title: String = "Child"
}

If you remove the @Environment(\.dismiss) it works, however I then can’t dismiss my view at all….

The issue stems from the fact that the dismiss value is not equatable and is therefore causing a cycle, because the view redraws endlessly when tapping the NavigationLink. It’s documented here https://hachyderm.io/@teissler/112533860374716961 and here https://forums.developer.apple.com/forums/thread/720096#:~:text=This%20is%20a%20problem%20if,to%20construct%20the%20presented%20view. but I can’t figure out how you’d resolve the requirement to use dismiss.


Solution

  • This seems to be a known bug with SwiftUI, present even in iOS version 17.

    To get around it, you can dismiss your views with presentationMode, like this

    struct DismissThisView: View {
        @Environment(\.presentationMode) var mode
    
        var body: some View {
            Button("Dismiss!") {
                mode.wrappedValue.dismiss()
            }
        }
    }
    
    

    This avoids having to reference the \.dismiss environment variable, which avoids the hanging behavior when trying to use NavigationLinks.