我有两个自定义类型,Student 和 Classroom,它们都符合 Identifiable 规范。当我更新班级中一名学生的姓名时,它会导致所有 Identifiable 视图(甚至数据没有变化的视图)更新并重新渲染。我预计,由于我的视图是 Identifiable 的,SwiftUI 会识别哪些视图需要更新并仅重新渲染那些视图。但是,使用下面的代码,如果我更改单个学生的姓名,所有视图都会更新并重新渲染,这不是我想要的。我只希望更新与更改的姓名相关的视图。我在这里做错了什么?
import SwiftUI
struct ContentView: View {
@State private var school: [Classroom] = [classroom1, classroom2]
var body: some View {
VStack {
Divider()
ForEach(school) { classroom in
ForEach(classroom.students) { student in
StudentView(student: student, action: { value in
if let classroomIndex: Int = findIndex(item: classroom, array: school) {
if let studentIndex: Int = findIndex(item: student, array: classroom.students) {
school[classroomIndex].students[studentIndex].name = value
}
}
})
}
Divider()
}
}
.padding()
}
}
func findIndex<U: Identifiable>(item: U, array: [U]) -> Int? {
return array.firstIndex(where: { element in (element.id == item.id) })
}
struct StudentView: View {
let student: Student
let action: (String) -> Void
var body: some View {
print("rendering for:", student.name)
return Text(student.name)
.onTapGesture {
action(student.name + " updated! ")
}
}
}
struct Student: Identifiable {
let id: UUID = UUID()
var name: String
}
struct Classroom: Identifiable {
let id: UUID = UUID()
var name: String
var students: [Student]
}
let classroom1: Classroom = Classroom(name: "Classroom 1", students: [Student(name: "a"), Student(name: "b"), Student(name: "c")])
let classroom2: Classroom = Classroom(name: "Classroom 2", students: [Student(name: "x"), Student(name: "y"), Student(name: "z")])
更新:
使用下面这个更新可以让代码正常工作而不存在额外渲染的问题:
struct StudentView: View, Equatable {
let student: Student
let action: (String) -> Void
var body: some View {
print("rendering for:", student.name)
return Text(student.name)
.onTapGesture {
action(student.name + " updated! ")
}
}
static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.student == rhs.student
}
}
struct Student: Identifiable, Equatable {
let id: UUID = UUID()
var name: String
static func ==(lhs: Self, rhs: Self) -> Bool {
return (lhs.id == rhs.id) && (lhs.name == rhs.name)
}
}
“哪些视图需要更新”不是由身份决定的,而是由平等决定的。
事实上, s 的任何身份都
StudentView
不会改变。因此按照您的逻辑,即使显示姓名已更新的学生的视图也不应该更新,因为它显示的学生仍然具有相同的id
。如果您的视图仅包含“简单”的结构值,SwiftUI 通常可以确定视图是否已更改(因此应该更新),即使视图及其包含的值不符合
Equatable
。这是一个演示:但是,如果你的视图有一个
(String) -> Void
,SwiftUI 总是认为你的视图已经发生了变化。毕竟,你如何判断一个 是否(String) -> Void
等于另一个(String) -> Void
?您可以使其
StudentView
自身符合Equatable
,因此 SwiftUI 确切地知道如何将 的旧版本StudentView
与新版本进行比较,并确定是否body
需要再次调用。@preconcurrency Equatable
这需要在 Swift 6 中实现,因为View
它与主参与者隔离。只要您不调用==
主参与者,这便是安全的。SwiftUI 将始终调用==
主参与者。或者,将 包装
(String) -> Void
在您自己的Equatable
实现中。在这里,我将(String) -> Void
和包装Student
成一个结构体。您也可以将其包装
(String) -> Void
到Equatable
始终返回 true 的实现中。这是来自@Sweeper 的非常有见地的回答,直接解决了您的代码的问题。
但是,如果您不想混淆 Equatable 和在数组中查找索引,则可以通过使用可观察类而不是结构体并将绑定传递给子视图来大大简化一切。
因此,不要
struct
:使用可观察类:
而不是循环查找索引:
传递绑定:
以下是完整代码: