SwiftUI是一个新的框架,可以让我们以声明性的方式构建用户界面。在SwiftUI中,可以使用ForEach视图来创建动态视图列表。ForEach是一种非常有用的视图类型,特别是当您需要用相同的方式处理一系列数据时。在本文中,我们将从多个方面对SwiftUI中的ForEach进行详细阐述。
一、ForEach的基础知识
ForEach是SwiftUI中用于构建动态列表的一种视图类型。与传统的iOS开发不同,SwiftUI采用声明性编程范式。这意味着我们告诉框架我们需要什么,然后让它自己进行处理。ForEach采用一个可迭代的数据源作为输入,然后将其映射到相应的视图层次结构中。
下面是一个ForEach的基本示例,它使用数组来构建一个简单的列表:
struct ContentView: View {
let names = ["Alice", "Bob", "Charlie", "Dave"]
var body: some View {
VStack {
ForEach(names, id: \.self) { name in
Text(name)
}
}
}
}
在上面的代码中,我们将字符串数组传递给ForEach构造函数。id参数用于指定每个元素的唯一标识符。在这种情况下,我们使用自身。ForEach迭代数组中的每个元素,并为每个元素创建一个Text视图。然后,我们将整个列表包装在一个垂直堆栈中,以便它们垂直排列。
二、ForEach中的数据操作
SwiftUI中的ForEach提供了一些方便的方法来操作数据源。下面是常用的一些方法:
1、添加元素
我们可以使用Swift数组的append方法添加新元素。在更新模型后,视图将自动调整以反映更改。
struct Student {
var name: String
}
struct ContentView: View {
@State private var students = [
Student(name: "Alice"),
Student(name: "Bob"),
Student(name: "Charlie")
]
var body: some View {
VStack {
ForEach(students, id: \.name) { student in
Text(student.name)
}
Button("Add student") {
self.students.append(Student(name: "Dave"))
}
}
}
}
2、删除元素
与添加元素类似,我们可以使用Swift数组的remove方法删除元素。更新视图和模型的方法与添加元素相同。
struct ContentView: View {
@State private var students = [
Student(name: "Alice"),
Student(name: "Bob"),
Student(name: "Charlie")
]
var body: some View {
VStack {
ForEach(students, id: \.name) { student in
Text(student.name)
}
Button("Remove student") {
self.students.remove(at: 0)
}
}
}
}
3、移动元素
移动元素需要两个索引:要移动的元素的当前位置和要将它移动到的新位置。
struct ContentView: View {
@State private var students = [
Student(name: "Alice"),
Student(name: "Bob"),
Student(name: "Charlie")
]
var body: some View {
VStack {
ForEach(students, id: \.name) { student in
Text(student.name)
}
Button("Move student") {
students.move(fromOffsets: IndexSet([0]), toOffset: 2)
}
}
}
}
三、ForEach的性能优化
SwiftUI使用了一种称为“Diffing”的算法来比较最新数据源与上一个版本的数据源,并确定需要更新的视图。在内部,SwiftUI会使用SwiftEquatable协议进行比较。
对于ForEach,我们可以使用id参数来告诉SwiftUI如何比较数据源中的元素。默认情况下,ForEach使用元素的内存地址作为标识符。如果数据源中的元素没有遵循SwiftEquatable协议,则默认行为可能不会按预期工作。通过传递一个可用于比较元素的键路径,我们可以自定义标识符。
struct Student: Identifiable {
let id = UUID()
var name: String
}
struct ContentView: View {
@State private var students = [
Student(name: "Alice"),
Student(name: "Bob"),
Student(name: "Charlie")
]
var body: some View {
VStack {
ForEach(students) { student in
Text(student.name)
}
Button("Add student") {
self.students.append(Student(name: "Dave"))
}
Button("Remove student") {
self.students.remove(at: 0)
}
Button("Move student") {
students.move(fromOffsets: IndexSet([0]), toOffset: 2)
}
}
}
}
1、使用Equatable来比较元素
首先,让我们看一下在没有指定id参数的情况下ForEach的默认行为:
struct Student {
var name: String
}
struct ContentView: View {
@State private var students = [
Student(name: "Alice"),
Student(name: "Bob"),
Student(name: "Charlie")
]
var body: some View {
VStack {
ForEach(students) { student in
Text(student.name)
}
Button("Add student") {
self.students.append(Student(name: "Dave"))
}
Button("Remove student") {
self.students.remove(at: 0)
}
}
}
}
当我们向上面的列表添加或删除学生时,我们会注意到,在删除学生时,视图滞后于模型。这是因为SwiftUI需要在模型中查找要删除的学生,而由于它们没有唯一的标识符,因此必须一一比较。这可能在数据集很大时变得非常慢。
为了解决这个问题,我们可以将Student标记为Equatable,并让SwiftUI使用SwiftEquatable协议进行比较。虽然这不是必需的,但它可以使性能更好。
struct Student: Equatable {
var name: String
}
struct ContentView: View {
@State private var students = [
Student(name: "Alice"),
Student(name: "Bob"),
Student(name: "Charlie")
]
var body: some View {
VStack {
ForEach(students) { student in
Text(student.name)
}
Button("Add student") {
self.students.append(Student(name: "Dave"))
}
Button("Remove student") {
self.students.remove(at: 0)
}
}
}
}
2、使用标识符来比较元素
如果我们的数据对象没有适合使用Equatable协议比较的属性,我们可以使用ForEach的id参数来指定一个键路径来比较元素。
struct Student {
var name: String
var id: Int
}
struct ContentView: View {
@State private var students = [
Student(name: "Alice", id: 1),
Student(name: "Bob", id: 2),
Student(name: "Charlie", id: 3)
]
var body: some View {
VStack {
ForEach(students, id: \.id) { student in
Text(student.name)
}
Button("Add student") {
self.students.append(Student(name: "Dave", id: 4))
}
Button("Remove student") {
self.students.remove(at: 0)
}
}
}
}
在上面的代码中,我们将id参数设置为我们可以使用的键路径(student.id)。
四、ForEach和可选类型
当我们处理可选类型时,我们需要使用ForEach的if语句来避免nil对象造成的崩溃。下面是一个使用可选类型的示例:
struct ContentView: View {
@State private var names = ["Alice", "Bob", nil, "Charlie"]
var body: some View {
VStack {
ForEach(names, id: \.self) { name in
if let name = name {
Text(name)
}
}
Button("Add name") {
self.names.append("Dave")
}
}
}
}
在上面的代码中,我们向名字数组中添加了一个空值。当我们构建ForEach时,我们使用if语句过滤掉了nil对象,并只创建非空Text视图。