如何停止 ForEach 中额外不必要的可识别视图渲染?

问题描述 投票:0回答:1

我有两种自定义类型,学生和课堂,它们都符合可识别的。当我更新班级中一名学生的姓名时,它会导致所有可识别视图(甚至数据没有更改的视图)更新并重新渲染。我预计,由于我的视图是可识别的,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
1个回答
0
投票

我预计,由于我的视图是可识别的,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
实现中。

© www.soinside.com 2019 - 2024. All rights reserved.