Home > OS >  How does SwiftUI's List update the selection binding?
How does SwiftUI's List update the selection binding?

Time:03-13

I'm really struggling to understand how the List(selection:) causes an update to the selection binding.

I am unpicking the Apple sample code at https://developer.apple.com/documentation/swiftui/building_a_great_mac_app_with_swiftui

The project is Session2/Part2-End/GardenApp.xcodeproj

There is a sidebar on the left, and the main detail view on the right. The ContentView looks like this:

struct ContentView: View {
    @EnvironmentObject var store: Store
    @SceneStorage("selection") private var selectedGardenID: Garden.ID?
    @AppStorage("defaultGarden") private var defaultGardenID: Garden.ID?

    var body: some View {
        NavigationView {
            Sidebar(selection: selection)
            GardenDetail(garden: selectedGarden)
        }
    }

    private var selection: Binding<Garden.ID?> {
        Binding(get: { selectedGardenID ?? defaultGardenID }, set: { selectedGardenID = $0 })
    }

    private var selectedGarden: Binding<Garden> {
        $store[selection.wrappedValue]
    }
}

and the Sidebar like this:

struct Sidebar: View {
    @EnvironmentObject var store: Store
    @SceneStorage("expansionState") var expansionState = ExpansionState()
    @Binding var selection: Garden.ID?

    var body: some View {
        List(selection: $selection) {
            DisclosureGroup(isExpanded: $expansionState[store.currentYear]) {
                ForEach(store.gardens(in: store.currentYear)) { garden in
                    SidebarLabel(garden: garden)
                        .badge(garden.numberOfPlantsNeedingWater)
                }
            } label: {
                Label("Current", systemImage: "chart.bar.doc.horizontal")
            }

            Section("History") {
                GardenHistoryOutline(range: store.previousYears, expansionState: $expansionState)
            }
        }
        .frame(minWidth: 250)
    }
}

Now the SidebarLabel view does not have any reference to the selection binding; yet, when you click on one of the labels, the selection updates and the binding propagates up to the ContentView and therefore the GardenDetai view is updated with the new selection.

My question is: how does clicking on one of the sidebar items cause the selection binding to be updated since there is no reference to the selection binding? Does the ForEach have anything to do with it?

If so, how can I add 'ad hoc' items that do not participate in a ForEach like this? If not, what is going on?

CodePudding user response:

List and ForEach work together here. The list sees that it has a ForEach over Garden in it, and the ForEach automatically provides a .tag(garden.id)

In a pure List form is would look like this, and probably shows much clearer what happens. I included an explicit .tag() but this will be generated automatically.

struct Sidebar2: View {
    @EnvironmentObject var store: Store
    @Binding var selection: Garden.ID?

    var body: some View {
        List(store.gardens(in: store.currentYear), selection: $selection) { garden in
                    SidebarLabel(garden: garden)
                        .tag(garden.id) // << not needed, will be done implicitly
                        .badge(garden.numberOfPlantsNeedingWater)
        }
        .frame(minWidth: 250)
    }
}

Because of the sections the Listis used as a wrapper around ForEach. But the principle is still the same. The ForEach hands its automatically generated id up to the List and sets the selection.

If you want to add custom elements to the Sidebar you can give them an explicit .tag ... you just have to find out where to decode these custom tags and what to display accordingly in the detail view.

CodePudding user response:

You can find more details in the documentation for List, check out the example under "Supporting Multi-Dimensional Lists".

But basically, this is what is happening:

  1. The line List(selection: $selection) stores the selected garden in the selection variable of the SideBar.

  2. The selection variable of SideBar is a binding from the selection variable in ContentView (see line Sidebar(selection: selection)), so when the former changes, the latter is updated too.

  3. The selectedGarden variable of ContentView is a computed property that returns the garden at the index of selection.wrappedValue - if selection's value changes, the selectedGarden returns a different value.

  4. The GardenDetail view shows the details according to the selectedGarden variable that is passed to it: GardenDetail(garden: selectedGarden).

Changing the selection in SideBar changes also the selection in ContenView, which makes the selectedGarden return a different garden which updates the GardenDetail view.

  • Related