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 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()
}
Answered by DTS Engineer in 814255022

it's best to drive the detail view behavior directly with the selection variable in the List.

for example:

struct ListView: View {
 var body: some View {
        NavigationSplitView {
            List(selection: $selection) {
                NavigationLink(item.id, value: item.id)
            }
        } detail: {
           if let selection {
              selection.destination
           } else { Text("Make a selection")
        }
    }
  }
}

Mixing and matching view-destination links with List selection as you have in your code snippet will result in undefined behavior

(Don't see a way to edit)

Probably should have mentioned that this is a Mac app

Accepted Answer

it's best to drive the detail view behavior directly with the selection variable in the List.

for example:

struct ListView: View {
 var body: some View {
        NavigationSplitView {
            List(selection: $selection) {
                NavigationLink(item.id, value: item.id)
            }
        } detail: {
           if let selection {
              selection.destination
           } else { Text("Make a selection")
        }
    }
  }
}

Mixing and matching view-destination links with List selection as you have in your code snippet will result in undefined behavior

OK, I've made the following changes, and (so far) it seems to be working... (Can you confirm this is what you were suggesting?)

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 {
        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"))
            }
        }
    }
}
Problems when using @Binding on a dynamic array to edit a list (and the items it contains)
 
 
Q