您的位置:

SwiftUI中的ForEach

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视图。