SwiftUI:使用`List`构建可编辑列表的教程

734 阅读5分钟

说SwiftUI的List ,相当于UIKit的UITableView ,这既是真的,也是假的。毫无疑问,List 提供了一种内置的方式来构建基于列表的UI,这些UI使用与使用UITableView 时相同的整体外观进行渲染--然而,当涉及到突变时,我们反而要求助于SwiftUI的核心引擎来构建和管理基于数据集合的视图--ForEach 类型。

移动和删除

一般来说,列表编辑通常涉及两种不同的编辑--针对项目的编辑和整个列表的编辑。从第一种变体开始,这里有一个使用 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 的状态管理系统指南

现在让我们假设我们也想添加对列表范围内突变的支持--特别是移动删除。好消息是,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")
        }
    }
}

然而,尽管我们的列表现在在技术上支持移动和删除,但目前还没有办法让用户进入 "编辑模式 "来开始移动项目。为了解决这个问题,让我们使用toolbar 修改器将SwiftUI内置的EditButton 的一个实例插入到我们应用程序的导航栏中:

struct TodoList: View {
    @Binding var items: [TodoItem]

    var body: some View {
        NavigationView {
            VStack {
                ...
            }
            .navigationTitle("Todo list")
            .toolbar { EditButton() }
        }
    }
}

请注意,如果你正在开发一个需要支持iOS 13的iOS应用,你需要使用现在已经被废弃的navigationBarItems 修改器,因为toolbar API是在iOS 14(以及苹果2020年的其他操作系统)中引入的。

有了这个变化,我们现在有了一个完全可编辑的列表,它既支持内联项目编辑,也支持整个列表的移动和删除。真的很好!

一个可重复使用的抽象概念

根据我们正在开发的应用程序,我们可能需要建立多个列表,每个列表都应该支持上述的编辑功能--虽然通过简单地复制和粘贴我们刚刚添加到TodoList 的修改器和EditButton 代码,当然可以实现这一点,但如果能有某种形式的可编辑列表,我们可以在我们的代码库中轻松地重复使用,可以说会更好。

所以,让我们建立一个吧!值得庆幸的是,由于 SwiftUI 的设计非常强调组合,实现一个完全可重用的EditableList 类型只需要将我们之前的编辑代码移到新视图的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 ,在它所定义的模块之外),但这样做的好处是,我们的EditableList API现在与SwiftUI的内置List ,工作方式相同,这将使两者之间的切换更加容易。

有了上述的新类型,当我们希望呈现一个可编辑的列表时,我们现在要做的就是用我们想让用户编辑的数组创建一个EditableList 实例--像这样:

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

真的很整洁!将我们的列表编辑代码封装在一个独立的类型中的另一个好处是,我们现在能够在一个单一的位置不断地添加编辑功能,而且我们所有的可编辑列表都将免费获得这些功能。例如,我们可能想增加对拖放和排序的支持,等等。

好了,到了奖励回合的时间了!只要我们的列表数据总是以Array 的形式出现,上述EditableList 的实现就可以完美地工作,可以说,如果它还能支持ListForEach 能够处理的任何Collection 类型(包括自定义类型),那就更好了。

为了实现这一点,我们必须改变我们的通用Element 类型,转而引用任何符合 SwiftUI 所要求的同一套标准库协议的Data 集合,以使我们的列表可编辑。不过这个实现必须要有 iOS 15 的支持,因为 SwiftUI 的Binding 类型在该操作系统版本中获得了对标准库的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() }
    }
}

我们现在有了一个完全通用的EditableList 实现,可以用于任何兼容的集合--但需要注意的是,它只与 iOS 15 及以后的系统兼容。但是,如果我们也需要支持早期的iOS版本,我们可能只需要使用我们之前基于Array 的版本,它是完全向后兼容的。

结论

List 可以说是最两极化的SwiftUI视图之一。一方面,它提供了大量的内置功能,使我们能够相对容易地建立列表,其外观和行为与苹果自己的应用程序以及iOS本身的列表完全一样。

然而,尽管自2019年推出以来,List 已经变得更加灵活,但使用它构建完全自定义的列表仍然相当困难。因此,对于这些用例,我们可能不得不回到UIKit或AppKit,这取决于我们的目标是什么平台。尽管如此,尽管它在视觉效果方面可能受到限制,但List 是 SwiftUI 中一个非常有用和强大的部分。

谢谢你的阅读!