说SwiftUI的List相当于UIKit的UITableView,这是既真又假。List确实提供了一种内置的方法来构造基于列表的ui,这些ui使用与UITableView相同的整体外观来呈现 - 然而,当涉及到变化时,我们不得不转而使用SwiftUI的核心引擎来构建和管理基于数据集合的视图——ForEach类型。
Moving and deleting
一般来说,列表编辑通常涉及两种不同的编辑——特定于项目的编辑和列表范围的编辑。从第一个变体开始,这里有一个使用Swift 5.5中引入的列表绑定语法的示例 - 我们正在渲染一个TodoList视图,用户可以使用TextField直接编辑每个项目的文本:
struct TodoItem: Identifiable {
let id: UUID
var title: String
}
struct TodoList: View {
@Binding var items: [TodoItem]
var body: some View {
NavigationView {
VStack {
List {
ForEach($items) { $item in
TextField("Title", text: $item.title)
}
}
TodoItemAddButton { newItem in
items.append(newItem)
}
}
.navigationTitle("Todo list")
}
}
}
在本例中,我们的TodoItem模型数组存储在视图之外,然后使用Binding传入。
现在让我们假设我们也想添加对列表范围的变化的支持——特别是移动和删除。好消息是,SwiftUI为这两个任务提供了内置的修饰符,但事实证明它们在List类型本身中不可用,而只能在ForEach中可用。
这是因为在SwiftUI中,List更像是一个样式组件,而不是一个负责管理子视图集合的视图。因此,每当我们希望改变这样的集合时,我们需要直接使用ForEach——像这样:
struct TodoList: View {
@Binding var items: [TodoItem]
var body: some View {
NavigationView {
VStack {
List {
ForEach($items) { $item in
TextField("Title", text: $item.title)
}
.onMove { indexSet, offset in
items.move(fromOffsets: indexSet, toOffset: offset)
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
...
}
.navigationTitle("Todo list")
}
}
}
然而,即使我们的列表现在在技术上支持移动和删除,目前还没有办法让用户进入“编辑模式”来开始移动项目。为了解决这个问题,让我们使用工具栏修饰符将SwiftUI的内置EditButton的一个实例插入到应用程序的导航栏中:
struct TodoList: View {
@Binding var items: [TodoItem]
var body: some View {
NavigationView {
VStack {
...
}
.navigationTitle("Todo list")
.toolbar { EditButton() }
}
}
}
注意,如果你正在开发一个需要支持iOS 13的iOS应用程序,你需要使用现在已经弃用的navigationBarItems修饰符,因为工具栏API已经在iOS 14(以及苹果2020年的其他操作系统)中引入。
有了这个更改,我们现在就有了一个完全可编辑的列表,它既支持内联项编辑,也支持列表范围内的移动和删除。真的很不错!
A reusable abstraction
根据我们正在开发的应用程序的类型,我们可能需要构建多个列表,每个列表都应该支持上述编辑功能集 -当然,只要简单地复制和粘贴我们刚刚添加到TodoList的修改器和EditButton代码,就可以实现这一点,可以证明,使用某种形式的可编辑列表会更好,这样我们就可以轻松地在代码库中重用它。
所以,让我们建造一个! 幸运的是,由于SwiftUI的设计非常强调组合,实现一个完全可重用的editableelist类型只需要将之前的编辑代码移到新视图的body中,然后添加一个初始化器,让我们注入我们想要呈现的数据,以及一个闭包,为列表中的每一项构造视图:
struct EditableList<Element: Identifiable, Content: View>: View {
@Binding var data: [Element]
var content: (Binding<Element>) -> Content
init(_ data: Binding<[Element]>,
content: @escaping (Binding<Element>) -> Content) {
self._data = data
self.content = content
}
var body: some View {
List {
ForEach($data, content: content)
.onMove { indexSet, offset in
data.move(fromOffsets: indexSet, toOffset: offset)
}
.onDelete { indexSet in
data.remove(atOffsets: indexSet)
}
}
.toolbar { EditButton() }
}
}
注意,我们不需要严格地为上面的类型实现自定义初始化式(除非我们想在定义它的模块外部将它作为public),但这样做的好处是,我们的editableelist API现在的工作方式与SwiftUI的内置List相同,这将使两者之间的切换更加容易。
有了上面的新类型,当我们想要渲染一个可编辑列表时,我们现在要做的就是用我们想让用户编辑的数组创建一个editableelist实例——像这样:
struct TodoList: View {
@Binding var items: [TodoItem]
var body: some View {
NavigationView {
VStack {
EditableList($items) { $item in
TextField("Title", text: $item.title)
}
TodoItemAddButton { newItem in
items.append(newItem)
}
}
.navigationTitle("Todo list")
}
}
}
真的整洁! 将列表编辑代码封装在独立类型中的另一个好处是,现在我们可以在单个位置中不断添加编辑功能,然后我们所有的可编辑列表将免费获得这些功能。例如,我们可能想要添加对拖放、排序等的支持。
好了,现在是奖励时间! 只要我们的列表数据总是以数组的形式出现,那么上面的EditableList实现就可以很好地工作,当然也可以让它支持list和ForEach能够使用的任何Collection类型(包括自定义集合)。
为了实现这一点,我们必须更改泛型Element类型,使其引用任何符合SwiftUI要求的标准库协议集的Data集合,以使列表可编辑。 不过,这个实现需要ios15,因为SwiftUI的Binding类型在那个OS版本中获得了对标准库RandomAccessCollection协议的支持:
@available(iOS 15, *)
struct EditableList<
Data: RandomAccessCollection & MutableCollection & RangeReplaceableCollection,
Content: View
>: View where Data.Element: Identifiable {
@Binding var data: Data
var content: (Binding<Data.Element>) -> Content
init(_ data: Binding<Data>,
content: @escaping (Binding<Data.Element>) -> Content) {
self._data = data
self.content = content
}
var body: some View {
List {
ForEach($data, content: content)
.onMove { indexSet, offset in
data.move(fromOffsets: indexSet, toOffset: offset)
}
.onDelete { indexSet in
data.remove(atOffsets: indexSet)
}
}
.toolbar { EditButton() }
}
}
我们现在有了一个完全通用的editableelist实现,可以用于任何兼容的集合——但要注意的是,它只兼容iOS 15及以后版本。但是,如果我们还需要支持早期的iOS版本,我们可能只需要使用之前的基于数组的版本,这是完全向后兼容的。
Conclusion
List可以说是SwiftUI上最两极分化的观点之一。一方面,它提供了许多内置功能,使我们能够相对容易地构建列表,这些列表的外观和行为与苹果自己的应用程序以及iOS本身的列表完全相同。
然而,尽管List自2019年推出以来变得更加灵活,但使用它构建完全自定义外观的列表仍然相当困难。因此,对于这些用例,我们可能不得不退回到UIKit或AppKit,这取决于我们的目标平台。不过,尽管在视觉效果方面可能有限,List仍然是SwiftUI非常有用和强大的一部分。