iosswiftui

Presence of @Environment dismiss causes list to constantly rebuild its content on scrolling


I need to build a list of TextFields where each field is associated with focus id, so that I can auto scroll to such a text field when it receives focus. In reality the real app is a bit more complex which also includes TextEditors and many other controls.

Now, I found out that if my view defines @Environment(\.dismiss) private var dismiss then the list is rebuilding all the time during manual scrolling. If I just comment out the line @Environment(\.dismiss) private var dismiss then there is no rebuilding of the list when I scroll. Obviously, I want to be able to dismiss my view when user clicks some button. In the real app it's even worse: during scrolling everything is lagging, I cannot get smooth scrolling. And my list is not huge it's just 10 items or so.

Here is a demo example:

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink {
                DismissListView()
            } label: {
                Text("Go to see the list")
            }
        }
    }
}

struct DismissListView: View {
    @Environment(\.dismiss) private var dismiss
    
    enum Field: Hashable {
        case line(Int)
    }
    
    @FocusState private var focus: Field?
    @State private var text: String = ""
    
    var body: some View {
        ScrollViewReader { proxy in
            List {
                let _ = print("body is rebuilding")
                
                Button("Dismiss me") {
                    dismiss()
                }
                
                Section("Section") {
                    ForEach((1...100), id: \.self) {num in
                        TextField("text", text: $text)
                            .id(Field.line(num))
                            .focused($focus, equals: .line(num))
                    }
                }
            }
            .listStyle(.insetGrouped)
            .onChange(of: focus) {_ in
                withAnimation {
                    proxy.scrollTo(focus, anchor: .center)
                }
            }
        }
    }
}

The questions are:

P.S. Demo app constantly outputs "body is rebuilding" when dismiss is defined and the list is scrolled, but if any text field gets a focus manually, then the "body is rebuilding" is not printed anymore even if the dismiss is still defined.


Solution

    1. I could make an assumption, but that would be really rather a guess (based on experience, observations, etc). In a fact, all WHYs like "why this sh... (bug) happens" should be asked on https://developer.apple.com/forums/ (there are Apple's engineers there) or reported to https://developer.apple.com/bug-reporting/

    2. A solution is to separate dismiss depenent part into dedicated view, so hiding it from parent body (and so do not affect it)

        struct DismissView: View {
            // visible only for this view !!
            @Environment(\.dismiss) private var dismiss
    
            var body: some View {
                Button("Dismiss me") {
                    // affects current context, so it does not matter
                    // in which sub-view is called
                    dismiss()           
                }
            }
        }
    
        var body: some View {
            ScrollViewReader { proxy in
                List {
                    let _ = print("body is rebuilding")
                    DismissView()   // << here !!
    
        // ... other code