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 List
is 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:
The line
List(selection: $selection)
stores the selected garden in theselection
variable of theSideBar
.The
selection
variable ofSideBar
is a binding from theselection
variable inContentView
(see lineSidebar(selection: selection)
), so when the former changes, the latter is updated too.The
selectedGarden
variable ofContentView
is a computed property that returns the garden at the index ofselection.wrappedValue
- ifselection
's value changes, theselectedGarden
returns a different value.The
GardenDetail
view shows the details according to theselectedGarden
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.