SwiftUI 列表视图 (list view) 和 UIKit 的 UITableView 很类似。在 SwiftUI 最初的版本中,Apple 的工程师已经把建立列表视图的过程变得轻而易举,我们不需要创建 prototype cell,也不需要委派 (delegate) 或 data source 的协议。我们只需要用几行代码,就可以使用客制化单元格来建构一个列表视图。
在 iOS 14 (和以上版本),Apple 继续改善列表视图,并添加了一些新功能。在本篇教学文章中,我们将会看看如何建构一个展开式列表视图 (expandable list view) 或大纲视图 (outline view),并探索 inset grouped 的列表样式 (list style)。
首先,让我们看看本篇教学的成品。我十分喜欢 La Marzocco,所以我用了它网站的导航菜单 (navigation menu) 为例子。以下的列表视图展示了菜单的大纲,使用者可以点击显示按钮来展开列表。
当然,你也可以用自己的实现方法,来建构这个大纲视图。但在最新版本的 SwiftUI 中,Apple 让开发者可以更简单地建构这种大纲视图,并自动适应于 iOS、iPadOS、和 macOS 版本。
要继续阅读本章的教学的话,请先从 https://www.appcoda.com/resources/swiftui/expandablelist-images.zip 下载示例图片。 然后,在 Xcode 使用 App 模板创建一个新的 SwiftUI 项目。 我将项目命名为 SwiftUIExpandableList,但您可以随意将名称设置为任何名称。
建立项目后,解压缩图像存档并将图像添加到 Assets。在项目导航器中,右键单击 SwiftUIExpandableList 并选择创建一个新文件档。 选择 Swift File 模板并将其命名为 MenuItem.swift。
要让列表视图可以展开,你只需要如此建立一个数据模型 (data model)。请加入以下代码至 MenuItem.swift
:
struct MenuItem: Identifiable {
var id = UUID()
var name: String
var image: String
var subMenuItems: [MenuItem]?
}
在上面的代码中,我们有一个用来建造菜单物件的结构 (struct)。要创建一个巢状列表 (nested list),关键就是要加入一个属性,包含子级的可选数组 (optional array) subMenuItems
。请注意,子级与其父级的类型 (type) 是一样的。
对于顶层 (top level) 菜单物件,我们可以如此创建一个 MenuItem
数组:
// Main menu items
let sampleMenuItems = [ MenuItem(name: "Espresso Machines", image: "linea-mini", subMenuItems: espressoMachineMenuItems),
MenuItem(name: "Grinders", image: "swift-mini", subMenuItems: grinderMenuItems),
MenuItem(name: "Other Equipment", image: "espresso-ep", subMenuItems: otherMenuItems)
]
我们会针对每个菜单物件,指定子菜单 (sub-menu) 物件的数组。如果没有子菜单物件,则可以省略 subMenuItems
参数,或传递 nil
值。我们可以这样定义子菜单物件:
// Sub-menu items for Espressco Machines
let espressoMachineMenuItems = [ MenuItem(name: "Leva", image: "leva-x", subMenuItems: [ MenuItem(name: "Leva X", image: "leva-x"), MenuItem(name: "Leva S", image: "leva-s") ]),
MenuItem(name: "Strada", image: "strada-ep", subMenuItems: [ MenuItem(name: "Strada EP", image: "strada-ep"), MenuItem(name: "Strada AV", image: "strada-av"), MenuItem(name: "Strada MP", image: "strada-mp"), MenuItem(name: "Strada EE", image: "strada-ee") ]),
MenuItem(name: "KB90", image: "kb90"),
MenuItem(name: "Linea", image: "linea-pb-x", subMenuItems: [ MenuItem(name: "Linea PB X", image: "linea-pb-x"), MenuItem(name: "Linea PB", image: "linea-pb"), MenuItem(name: "Linea Classic", image: "linea-classic") ]),
MenuItem(name: "GB5", image: "gb5"),
MenuItem(name: "Home", image: "gs3", subMenuItems: [ MenuItem(name: "GS3", image: "gs3"), MenuItem(name: "Linea Mini", image: "linea-mini") ])
]
// Sub-menu items for Grinder
let grinderMenuItems = [ MenuItem(name: "Swift", image: "swift"),
MenuItem(name: "Vulcano", image: "vulcano"),
MenuItem(name: "Swift Mini", image: "swift-mini"),
MenuItem(name: "Lux D", image: "lux-d")
]
// Sub-menu items for other equipment
let otherMenuItems = [ MenuItem(name: "Espresso AV", image: "espresso-av"),
MenuItem(name: "Espresso EP", image: "espresso-ep"),
MenuItem(name: "Pour Over", image: "pourover"),
MenuItem(name: "Steam", image: "steam")
]
准备好数据模型后,我们可以编写代码以呈现列表视图。List
视图现在有了可选的 children
参数。所以如果有任何子物件,你可以提供其 key path \.subMenuItems
。 然后,SwiftUI 将递归 (recursively) 查找子菜单物件,并以大纲形式显示。打开 ContentView.swift
并在 body
中插入以下代码:
List(sampleMenuItems, children: \.subMenuItems) { item in
HStack {
Image(item.image)
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
Text(item.name)
.font(.system(.title3, design: .rounded))
.bold()
}
}
在 List
视图闭包中,我们会描述每一行的外观。在示例代码中,我们使用了 HStack
布局图像和文本视图。如果你已经在 ContentView
中正确地添加了代码的话,SwiftUI 应该会如此呈现大纲视图:
要测试App,请在模拟器或预览画布中运行它。 您可以点击披露指示器以显示子菜单。
在 iOS 15 中,Apple 将列表视图的默认样式设置为 Inset Grouped,其中分组的部分以圆角嵌入。 如果要将其切换回普通列表样式,可以将 .listStyle
修饰器附加到 List
视图并将其值设置为 .plain
,如下所示:
List {
...
}
.listStyle(.plain)
如果你没有加错位置,列表视图现在应该修改为普通样式。
正如您在前面的示例中所见,使用 List
视图创建大纲视图非常容易。 但是,如果您想更好地控制大纲视图的外观(例如添加部分标题),则需要使用 OutlineGroup
。 这个新视图是自 iOS 14 才引入的,用于呈现数据的层次结构。
如果您已明白如何构建可扩展的列表视图,“OutlineGroup”的用法也非常相似。 例如,以下代码允许您构建相同的可扩展列表视图,如图 28.1 所示:
List {
OutlineGroup(sampleMenuItems, children: \.subMenuItems) { item in
HStack {
Image(item.image)
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
Text(item.name)
.font(.system(.title3, design: .rounded))
.bold()
}
}
}
和 List
视图类似,您只需要在建立OutlineGroup
时传入要显示的项目并指定子菜单项(或子项)的键路径(Key path)。
有了 OutlineGroup
,您可以更好地控制大纲视图的外观。 例如,我们希望将顶级菜单项显示为节标题。 你可以这样写代码:
List {
ForEach(sampleMenuItems) { menuItem in
Section(header:
HStack {
Text(menuItem.name)
.font(.title3)
.fontWeight(.heavy)
Image(menuItem.image)
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
}
.padding(.vertical)
) {
OutlineGroup(menuItem.subMenuItems ?? [MenuItem](), children: \.subMenuItems) { item in
HStack {
Image(item.image)
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
Text(item.name)
.font(.system(.title3, design: .rounded))
.bold()
}
}
}
}
}
在上面的代码中,我们使用 ForEach
来遍历菜单项。 我们将顶层项目呈现为节标题。 对于其余的子菜单项,我们依靠“OutlineGroup”来创建数据层次结构。 如果您修改 ContentView.swift
内的代码,您应该会看到如图 28.4 所示的大纲视图。
同样地,如果您更喜欢使用普通列表样式,可以将 listStyle
修饰器附加到 List
视图:
.listStyle(.plain)
然后你会得到图 28.5 的结果。
在大纲视图中,您可以通过点击显示指示器来显示/隐藏子菜单。 无论您使用 List
还是 OutlineGroup
来实现可展开列表,iOS 14 中引入的名为 DisclosureGroup
的新视图都支持这种“展开和折叠”功能。
公开组视图(Disclosure Group View)是为显示或隐藏另一个内容视图而设计。 虽然 DisclosureGroup
会自动嵌入到 OutlineGroup
中,但您也可以独立使用此视图。 例如,您可以使用以下代码来显示和隐藏问题和答案:
DisclosureGroup(
content: {
Text("Absolutely! You are allowed to reuse the source code in your own projects (personal/commercial). However, you're not allowed to distribute or sell the source code without prior authorization.")
.font(.body)
.fontWeight(.light)
},
label: {
Text("1. Can I reuse the source code?")
.font(.body)
.bold()
.foregroundColor(.black)
}
)
公开组视图有两个参数:label 和 content。 在上面的代码中,我们在 label
参数中指定了问题,在 content
参数中指定了答案。 图 28.6 显示了结果。
在默认的情况下,公开组视图处于隐藏模式。 要显示内容视图,请点击显示指示器(>)以将显示组视图切换到“展开”状态。
另外,您可以通过传一个绑定来控制 DisclosureGroup
的状态,该绑定直接控制披露指示器的状态(展开或折叠),如下所示:
struct FaqView: View {
@State var showContent = true
var body: some View {
DisclosureGroup(
isExpanded: $showContent,
content: {
...
},
label: {
...
}
)
.padding()
}
}
DisclosureGroup
视图允许您更好地控制披露指标的状态。 你的练习是建立一个类似于图 28.7 所示的 FAQ 屏幕。
使用者可以点击披露指示器来显示或隐藏单个问题。 此外,App也提供了一个“Show all”按钮来展开所有问题并立即显示答案。
在本章中,我介绍了 SwiftUI 的一些新功能。 正如您在演示中看到的那样,构建大纲视图或可扩展列表视图一点也不难。 您需要做的就是定义一个正确的数据模型。 List 视图处理其余部分并呈现大纲视图。 最重要的是,新的修改提供了 OutlineGroup
和 DisclosureGroup
供您进一步客制化大纲视图。
在本章所准备的示例档中,有最后完整的 Xcode 项目,可供你下载参考:
https://www.appcoda.com/resources/swiftui4/SwiftUIExpandableList.zip
请注意,您可以参考 FaqView.swift
来获得练习的解决方案。