ForEach
视图允许在循环中创建其他视图。例如想要遍历一组名称,将每个名称转化成文本视图;或遍历一组菜单项,将每个名称显示为图像。
ForEach
可以为数组中的每个元素创建单独的视图ForEach
将为它循环的每个项目运行一次闭包,并传入当前循环项目ForEach
支持绕过 SwiftUI 的 10 个子项限制,ForEach
本身是 10 个子项之一,但它里面的视图不是。它创建的视图不计入该限制// 例子:foreach 可以遍历一个半闭范围。后面闭包中的唯一参数 number ,代表前面范围中提取的一个个值
ForEach(0..<100) { number in
Text("Row \\(number)")
}
// 例子:foreach 可以直接遍历数组
let agents = ["Cyril", "Lana", "Pam", "Sterling"]
ForEach(agents, id: \\.self) { agent in
Text(agent)
}
ForEach(emojis[0..<emojiCount], id: \\.self){ emoji in
CardView(content: emoji)
}
// 省略闭包参数名:因为传入的是闭包,所以可以使用闭包参数的简写语法,如下所示:
ForEach(0..<agents.count) { Text(agents[$0]) }
ForEach
接受的数据类型是: Range<Int>
,这是一个整数范围,还有第二种相似的类型称为 ClosedRange<Int>
,这就是导致出错的原因。当我们写 0..<5
时,我们得到一个 Range<Int>
,但是当我们写 0...5
时,我们得到一个 ClosedRange<Int>
尽管它看起来与我们相似,但 Swift 认为这两种范围类型是不同的,因此我们不能将封闭范围与 ForEach
一起使用。(希望未来可以改变这个)
// 以下代码正确,半闭范围
ForEach( 0..<5 ) {
Text("Row \\($0)")
}
// 以下代码报错,因为是全闭范围
ForEach( 0...5 ) {
Text("Row \\($0)")
}
ForEach 的 ID 属性是必须填写的,因为 SwiftUI 需要能够唯一地识别屏幕上的每个视图,以便它可以检测到事物何时发生变化。
当在 SwiftUI 中创建静态视图时(我们硬编码 VStack
,TextField
,Button
等),SwiftUI 可以准确地看到拥有哪些视图,并且能够控制它们、赋予它们动画等。但是当使用 List
或 ForEach
制作动态视图时,SwiftUI 需要知道它如何唯一地标识每个项目,否则它将很难比较视图层次结构以找出发生了什么变化。
真正解决 ID 唯一性问题的做法是:生成 UUID 的唯一识别码,并遵循 Identifiable 协议。
// UUID 是长十六进制字符串,例如:08B15DB4-2F02-4AB8-A965-67A9C90D8A44。所以,那就是八位,四位,四位,四位,然后十二位,其中唯一的要求是第三个块的第一个数字是4。如果我们减去固定的 4,我们最终会得到 31 个数字,每个数字都可以是 16 个值之。如果在 10 亿年里每秒生成 1 个 UUID,才可能有机会生成重复项。
// 1: 给每个实例自动生成UUID,用UUID函数随机产生通用的识别码,作为通用的唯一标识
struct Restaurant{
var id = UUID()
var name: String
var image: String
}
// 2. 使用 Restaurant 的 id 属性作为唯一标识
ForEach(restaurants, id: \\.id){
restaurant in
Image(restaurant.image)
Text(restaurant.name)
}
还应该让结构遵循 Identifiable 协议,该协议只有一个要求,即该类型必须具备某种 ID 作为唯一识别码。
// 为什么要遵循这个协议,不遵循不是也有效吗?因为遵循 Identifiable 协议,只有一个要求,即必须有一个名为 id 的属性,其中包含唯一标识符。所以当结构体遵循了该协议,就不再需要告诉 ForEach 使用哪个属性作为标识符了,它知道将会有一个 id 属性,并且它将是唯一的。
// 所以遵循 Identifiable 协议,可以把 ForEach 中的 ID 参数部分省略掉。
// 因此,代码可以修改为如下所示:
struct Restaurant: Identifiable {
var id = UUID()
var name: String
var image: String
}
//id部分的参数已经被省略了。
ForEach(restaurants){
restaurant in
Image(restaurant.image)
Text(restaurant.name)
}
当遍历复杂数据时,如果不用 UUID ,也可以用对象里面的属性值作为ID,写作 \\.name
。但如何确保这些属性值是唯一的是个难题。
struct ExpenseItem {
let name: String
let type: String
let amount: Double
}
ForEach(expenses.items, id: \\.name) {
item in
Text(item.name)
}
当 ForEach 遍历的是一个简单的数组时(例如:字符串数组、整数数组),由于无法使它们符合 Identifiable
协议,也就没有 ID 属性。于是我们告诉 Swift,字符串本身就是每个元素的唯一标识符。于是我们传入 \\.self
作为其 id
参数。
ForEach(students, id:\\.self) { Text($0) }
<aside>
💡 该功能仅适用于实现 DynamicViewContent
协议的视图。 目前,唯一符合 DynamicViewContent
协议的视图是 ForEach
视图。 因此,这些功能仅在 ForEach
视图上可用,而不能在 List
列表视图上使用。
</aside>
当【表单Form】或【列表List】中有行时,SwiftUI 喜欢假设【整行】本身是可点击的。这使得用户可以更轻松地进行选择,因为他们可以点击行中的任意位置来触发其中的按钮。在下面例子中,我们有多个按钮,因此 SwiftUI 按顺序点击所有按钮 - rating
设置为 1,然后是 2,然后是 3、4 和 5,这就是为什么它最终都是 5 。
// contentView
@State var rating: Int = 3
Form{
Section("Write a review"){
RatingView(rating: $rating)
}
}
// subView
@Binding var rating: Int
HStack {
ForEach( 1..<maximumRating + 1, id: \\.self) { number in
Button {
rating = number
print(rating)
} label: {
image(for: number).foregroundStyle(number > rating ? offColor : onColor)
}
}
}
// 这时,可以通过在子视图中附加到整个 HStack 的额外修饰符,来禁用整个“点击行以触发其按钮”行为:
HStack { ... }
.buttonStyle(.plain)
如果希望 onDelete
正常工作,需要创建一个能接收单个 IndexSet
类型参数的方法。这有点像一组整数,只不过它是经过排序的,它只是告诉我们 ForEach
中应删除的所有项目的具体位置。
struct ContentView: View {
// 定义了一个空的整数数组,并加上了 @State
@State private var numbers = [Int]()
@State private var currentNumber = 1
var body: some View {
NavigationStack{
VStack {
List {
ForEach(numbers, id: \\.self) {
Text("Row \\($0)")
}
// 在ForEach后添加onDelete修饰符,告诉SwiftUI当想要从ForEach中删除数据时调用该方法
.onDelete(perform: removeRows)
}
Button("Add Number") {
numbers.append(currentNumber)
currentNumber += 1
}
}
.toolbar {
EditButton()
// EditButton() 是一个内置函数,返回一个视图(按钮)。该视图将自动切换列表中的编辑模式。
// 它的文本显示“编辑”,然后点击时,您将看到移动手柄出现在行上,按钮文本显示“完成”。
}
}
}
// 声明删除行的方法:接收单个 IndexSet 参数的方法
// 例子中 ForEach 完全是从单个数组创建的,所以实际上可以将该索引集直接传递到 numbers 数组;
// 它有一个特殊的 remove(atOffsets:) 方法接受索引集
func removeRows(at offsets: IndexSet) {
numbers.remove(atOffsets: offsets)
}
// remove(atOffsets:) 方法是一个 mutating 方法,它用于从数组中移除元素。而你尝试在不可变的值上调用它
// 为了解决这个问题,所以要使用 @State 修饰符来标记 numbers 属性为可变状态,以允许在视图中修改它
}
// 例子2
@State var data = ["Swipe to Delete", "Practice Coding", "Grocery shopping", "Get tickets"]
// 在 onDelete 的閉包,它會傳遞一個 indexSet,儲存被刪除的索引。
// 然後以 indexSet 來呼叫 remove 方法來刪除在 restaurants 陣列中的特定項目
func delete(at indexes: IndexSet) {
if let first = indexes.first {
data.remove(at: first)
}
}
onMove
修饰符后面有 source
和 destination
两个参数。
struct List_MoveRow : View {
@State var data = ["Hit the Edit button to reorder", "Practice Coding", "Grocery shopping", "Get tickets", "Clean house", "Do laundry", "Cook dinner", "Paint room"]
var body: some View {
NavigationView {
List {
ForEach(data, id: \\.self) {
datum in
Text(datum).font(Font.system(size: 24)).padding()
}
//Move修饰器是加到ForEach上,不是加到List
.onMove { source, destination in
data.move(fromOffsets: source, toOffset: destination)
}
}
.navigationTitle("To Do")
//工具栏上增加编辑按钮
.toolbar {
ToolbarItem { EditButton() }
}
}
// 改变工具栏上按钮的颜色
.tint(.green)
}
}
// 例子2:
struct List_Delete : View {
// 如果希望操作后更新UI,记得在数据参数前加上 @State 关键词
@State var data = ["Swipe to Delete", "Practice Coding", "Grocery shopping", "Get tickets"]
var body: some View {
List {
Section(header: Text("To Do").padding()) {
ForEach(data, id: \\.self) {
datum in
Text(datum).font(Font.system(size: 24)).padding()
}
// 添加移动
.onMove(perform: moveRow)
}
}
}
// 同理,排序的闭包
func moveRow(from indexes: IndexSet, to destination: Int) {
if let first = indexes.first {
data.insert(data.remove(at: first), at: destination)
}
}
}
如果您已将 SwiftUI 列表视图配置为支持删除或编辑其项目,则可以通过在某处添加 EditButton
来允许用户切换列表视图的编辑模式。
// 例如:在 toolbar 工具栏添加 EditButton() ,实现同时编辑多行数据
.toolbar {
EditButton()
}
struct ContentView: View {
// students 数组不需要用 @State 标记,因为它是一个常量;它不会改变
let students = ["Harry", "Hermione", "Ron"]
// selectedStudent 属性以值“Harry”开头,但可以更改,这就是它标记为 @State 的原因
@State private var selectedStudent = "Harry"
var body: some View {
NavigationStack {
Form {
Picker("Select your student", selection: $selectedStudent) {
ForEach(students, id: \\.self) {
Text($0)
}
}
}
}
}
}