我有两种自定义类型,学生和课堂,它们都符合可识别的。当我更新班级中一名学生的姓名时,它会导致所有可识别视图(甚至数据没有更改的视图)更新并重新渲染。我预计,由于我的视图是可识别的,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")])
我预计,由于我的视图是可识别的,SwiftUI 会识别哪些视图需要更新,并且只重新渲染这些视图。
“哪些视图需要更新”不是由身份决定,而是由平等决定。
事实上,
StudentView
的身份从未改变过。因此,根据您的逻辑,即使显示姓名已更新的学生的视图也不应更新,因为它显示的学生仍然具有相同的id
。
如果您的视图仅包含“简单”结构值,SwiftUI 通常可以确定视图是否已更改(因此应该更新),即使视图及其包含的值不符合
Equatable
。这是一个演示:
struct ContentView: View {
@State var classroom = classroom1
var body: some View {
// you don't need findIndex, just pass a binding to ForEach!
ForEach($classroom.students) { $student in
HStack {
SimpleStudentView(student: student)
Button("Update") {
// only one SimpleStudentView gets updated when you tap the button
student.name += " updated"
}
}
}
}
}
struct SimpleStudentView: View {
let student: Student
var body: some View {
print("updated", student.name)
return Text(student.name)
}
}
但是,如果你的视图有
(String) -> Void
,SwiftUI 总是认为你的视图已经改变。毕竟,you 如何确定一个 (String) -> Void
是否等于另一个 (String) -> Void
?
您可以使
StudentView
本身与 Equatable
保持一致,这样 SwiftUI 就能准确地知道如何将 StudentView
的旧版本与新版本进行比较,并确定是否需要再次调用 body
。
在 Swift 6 中这需要是
@preconcurrency Equatable
,因为 View
与主要参与者是隔离的。只要 you 不叫 ==
离开主角,这就是安全的。 SwiftUI 将始终对主角调用 ==
。
struct Student: Identifiable, Equatable { // Student should conform to Equatable!
let id: UUID = UUID()
var name: String
}
struct StudentView: View, @preconcurrency Equatable {
let student: Student
let action: (String) -> Void
var body: some View {
print("updated", student.name)
return Text(student.name)
.onTapGesture {
action(student.name + " updated! ")
}
}
static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.student == rhs.student
}
}
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) {
student.name = $0
}
}
Divider()
}
}
.padding()
}
}
或者,将
(String) -> Void
包装在您自己的 Equatable
实现中。在这里,我将 (String) -> Void
和 Student
包装到一个结构中。
struct StudentAndAction: Equatable {
let student: Student
let action: (String) -> Void
static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.student == rhs.student
}
}
struct StudentView: View {
let studentAndAction: StudentAndAction
init(student: Student, action: @escaping (String) -> Void) {
self.studentAndAction = StudentAndAction(student: student, action: action)
}
var body: some View {
print("updated", studentAndAction.student.name)
return Text(studentAndAction.student.name)
.onTapGesture {
studentAndAction.action(studentAndAction.student.name + " updated! ")
}
}
}
您还可以just将
(String) -> Void
包装到始终返回true的Equatable
实现中。