swiftuibinding

Problems when using @Binding on a dynamic array to edit a list (and the items it contains)


I'm running into an issue where, eventually, something goes "wonky" on some detail items won't present the DetailView when they are clicked on.

At first, everything is fine, I can edit (and see results reflected in realtime). I can reorder the list, and I can add Detail items to the list. However, after a while, I start seeing the default detail view (Text("Select a detail item to view")) whenever I click on some items. I've not been able to predict where a failure will occur.

In the real app I have a persisted JSON document that never seems to get corrupted. And reflects the edits I perform.

With this sample app, I can pretty reliably trigger the issue by moving a detail item in the list, and then adding a detail item, but, it will eventually fail without those actions.

Am I doing something that's "forbidden"?

(You can create a new Mac app and replace the ContentView.swift with the code below to run this and play with it)

import SwiftUI

struct ContentView: View {
    @State var main = Main()
    var body: some View {
        NavView(main: $main)
            .onAppear {
                for index in 1..<11 {
                    main.details.append(
                        Detail(
                            title: "Detail \(index)",
                            description: "Description \(index)"))
                }

            }
    }
}

struct NavView: View {
    @Binding var main: Main
    var body: some View {
        NavigationSplitView {
            ListView(main: $main)
        } detail: {
            Text("Select a detail item to view")
        }
    }
}

struct ListView: View {
    @Binding var main: Main
    @State private var addedDetailCount = 0
    var body: some View {
        List($main.details, editActions: .move) { $detail in
            NavigationLink(destination: DetailView(detail: $detail)) {
                Text("\(detail.title)")
            }
        }
        .toolbar {
            Button(
                LocalizedStringKey("Add Detail"), systemImage: "plus"
            ) {
                addedDetailCount += 1
                main.details.append(
                          .init(title: "new Detail \(addedDetailCount)", description: "description"))
            }
        }
    }

}

struct DetailView: View {
    @Binding var detail: Detail
    var body: some View {
        Form {
            Text("id: \(detail.id)")
            TextField("Title", text: $detail.title)
            TextField("Detail", text: $detail.description)
        }
        .padding()
    }
}

struct Main {
    var details = [Detail]()
}

struct Detail: Identifiable {
    var id = UUID()
    var title: String
    var description: String
}

#Preview {
    ContentView()
}

Solution

  • per Apple Developer support, "Mixing and matching view-destination links with List selection as you have in your code snippet will result in undefined behavior"

    I needed to use the selection parameter on the List view.

    This revised code, seems to work without issue:

    import SwiftUI
    
    struct ContentView: View {
        @State var main = Main()
        var body: some View {
            NavView(main: $main)
                .onAppear {
                    for index in 1..<11 {
                        main.details.append(
                            Detail(
                                title: "Detail \(index)",
                                description: "Description \(index)"))
                    }
    
                }
        }
    }
    
    struct NavView: View {
        @Binding var main: Main
        @State private var selection: UUID?
        var body: some View {
            NavigationSplitView {
                ListView(main: $main, selection: $selection)
            } detail: {
                if let selection {
                    DetailView(
                        detail: $main.details.first(where: { $0.id == selection })!)
                } else {
                    Text("Select a detail item to view")
                }
            }
        }
    }
    
    struct ListView: View {
        @Binding var main: Main
        @State private var addedDetailCount = 0
        @Binding var selection: UUID?
        var body: some View {
            // primary change is here --------------\/
            List($main.details, editActions: .move, selection: $selection) {
                $detail in
                NavigationLink(detail.title, value: detail)
            }
            .toolbar {
                Button(
                    LocalizedStringKey("Add Detail"), systemImage: "plus"
                ) {
                    addedDetailCount += 1
                    main.details.append(
                        .init(
                            title: "new Detail \(addedDetailCount)",
                            description: "description"))
                }
            }
        }
    }
    
    struct DetailView: View {
        @Binding var detail: Detail
        var body: some View {
            Form {
                Text("id: \(detail.id)")
                TextField("Title", text: $detail.title)
                TextField("Detail", text: $detail.description)
            }
            .padding()
        }
    }
    
    struct Main {
        var details = [Detail]()
    }
    
    struct Detail: Identifiable, Hashable {
        var id = UUID()
        var title: String
        var description: String
    }
    
    #Preview {
        ContentView()
    }