SwiftUI 的 List 如何更新选择绑定?

How does SwiftUI's List update the selection binding?

我真的很难理解 List(selection:) 如何导致对选择绑定的更新。

我正在 https://developer.apple.com/documentation/swiftui/building_a_great_mac_app_with_swiftui

解开 Apple 示例代码

项目是Session2/Part2-End/GardenApp.xcodeproj

左边是侧边栏,右边是主要的细节视图。 ContentView 看起来像这样:

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 = [=10=] })
    }

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

侧边栏是这样的:

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)
    }
}

现在 SidebarLabel 视图没有任何对选择绑定的引用;但是,当您单击其中一个标签时,选择会更新并且绑定会传播到 ContentView,因此 GardenDetai 视图会更新为新选择。

我的问题是:由于没有对选择绑定的引用,单击其中一个侧边栏项目如何导致选择绑定更新? ForEach 和它有什么关系吗?

如果是这样,我如何添加 'ad hoc' 不参与 ForEach 的项目?如果不是,那是怎么回事?

您可以在 documentation for List 中找到更多详细信息,查看“支持 Multi-Dimensional 列表”下的示例。

但基本上,这就是正在发生的事情:

  1. List(selection: $selection)将选择的花园存储在SideBarselection变量中。

  2. SideBarselection变量是ContentViewselection变量的绑定 (参见第 Sidebar(selection: selection) 行),因此当前者发生变化时,后者也会更新。

  3. ContentViewselectedGarden 变量是计算得到的 属性 return 索引为 selection.wrappedValue 的花园 -如果 selection 的值发生变化,selectedGarden return 将是一个不同的值。

  4. GardenDetail 视图根据传递给它的 selectedGarden 变量显示详细信息:GardenDetail(garden: selectedGarden).

改变 SideBar 中的 selection 也会改变 ContenView 中的 selection,这使得 selectedGarden return 成为一个不同的花园更新 GardenDetail 视图。

ListForEach 在这里一起工作。列表看到里面有个ForEach over Garden,ForEach自动提供了一个.tag(garden.id)

在纯 List 形式中,它看起来像这样,并且可能更清楚地显示发生了什么。我包含了一个明确的 .tag() 但这将自动生成。

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)
    }
}

由于这些部分,List 被用作 ForEach 的包装。但是原理还是一样的。 ForEach 将其自动生成的 id 交给 List 并设置 selection.

如果您想将自定义元素添加到侧边栏,您可以给它们一个明确的 .tag ...您只需要找出在哪里解码这些自定义标签以及在详细视图中相应地显示什么.