SwiftUI - 使用 ForEach 时,您应该在子视图中使用 `@State var` 还是 `let`

Posted

技术标签:

【中文标题】SwiftUI - 使用 ForEach 时,您应该在子视图中使用 `@State var` 还是 `let`【英文标题】:SwiftUI - Should you use `@State var` or `let` in child view when using ForEach 【发布时间】:2021-12-24 15:38:35 【问题描述】:

我认为我在理解 @State 的确切含义方面存在差距,尤其是在显示来自 ForEach 循环的内容时。

我的场景:我创建了最小的可重现示例。下面是带有ForEach 循环的父视图。每个子视图都有一个NavigationLink

// Parent code which passes a Course instance down to the child view - i.e. CourseView
struct ContentView: View 
    
    @StateObject private var viewModel: ViewModel = .init()
    
    var body: some View 
        NavigationView 
            VStack 
                ForEach(viewModel.courses)  course in
                    NavigationLink(course.name + " by " + course.instructor) 
                        CourseView(course: course, viewModel: viewModel)
                    
                
            
        
    


class ViewModel: ObservableObject 
    @Published var courses: [Course] = [
        Course(name: "CS101", instructor: "John"),
        Course(name: "NS404", instructor: "Daisy")
    ]


struct Course: Identifiable 
    var id: String = UUID().uuidString
    var name: String
    var instructor: String

实际困境:我为CourseView 尝试了两种变体,一种使用let 常量,另一种使用@State var 用于course 字段。下面代码中的其他 cmets。

当导航链接打开时,带有let 常量的那个成功地更新了子视图。但是,@State var 不会更新视图。

struct CourseView: View 
    
    // Case 1: Using let constant (works as expected)
    let course: Course

    // Case 2: Using @State var (doesn't update the UI)
    // @State var course: Course

    @ObservedObject var viewModel: ViewModel
    
    var body: some View 
        VStack 
            Text("\(course.name) by \(course.instructor)")
            Button("Edit Instructor", action: editInstructor)
        
    
    
    // Case 1: It works and UI gets updated
    // Case 2: Doesn't work as is. 
    //    I've to directly update the @State var instead of updating the clone -
    //    which sometimes doesn't update the var in my actual project 
    //    (that I'm trying to reproduce). It definitely works here though.
    private func editInstructor() 
        let instructor = course.instructor == "Bob" ? "John" : "Bob"
        var course = course
        course.instructor = instructor
        save(course)
    
    
    // Simulating a database save, akin to something like GRDB
    // Here, I'm just updating the array to see if ForEach picks up the changes
    private func save(_ courseToSave: Course) 
        guard let index = viewModel.courses.firstIndex(where:  $0.id == course.id ) else 
            return
        
        viewModel.courses[index] = courseToSave
    

我正在寻找的是需要循环遍历模型数组并且模型从子视图中在 DB 中更新的场景的最佳实践。

【问题讨论】:

您只是在使用 var 进行初始化,这就是问题所在,让您推送视图以进行更新,您可以使用 let 或使用绑定!取决于您的孩子是否会更新该值。 你应该使用@Binding @swiftPunk 帮助我理解。因此,当父数组更新时,let 版本将获取新值。但是为什么@State 不这样做呢?那里有点困惑。 @JoakimDanielson 在子视图中确实发生了很多变化,而父视图并没有更新那么多。我倾向于让方法。 @SiddharthKamaria:如果你在苹果视频中注意到State 他们都使用private 这意味着任何用State 包裹的值它是一种独立且自动的,只对属于的视图负责,有时我们可以初始化 State 值但我们只是初始化它!之后,如果我们尝试重新初始化,它不会被初始化,因为它已经被初始化了!除非我们杀死视图并重新初始化 【参考方案1】:

这是一个正确的方法,不要忘记我们不需要在 View 中放置逻辑!视图应该是虚拟的!

struct ContentView: View 
    
    @StateObject private var viewModel: ViewModel = ViewModel.shared
    
    var body: some View 
        NavigationView 
            VStack 
                ForEach(viewModel.courses)  course in

                    NavigationLink(course.name + " by " + course.instructor, destination: CourseView(course: course, viewModel: viewModel))

                
            
        
    





struct CourseView: View 

    let course: Course

    @ObservedObject var viewModel: ViewModel
    
    var body: some View 
        VStack 
            Text("\(course.name) by \(course.instructor)")
            Button("Update Instructor", action:  viewModel.update(course) )
        
    
    




class ViewModel: ObservableObject 
    static let shared: ViewModel = ViewModel()
    @Published var courses: [Course] = [
        Course(name: "CS101", instructor: "John"),
        Course(name: "NS404", instructor: "Daisy")
    ]
    

    func update(_ course: Course) 
        guard let index = courses.firstIndex(where:  $0.id == course.id ) else 
            return
        

        courses[index] = Course(name: course.name, instructor: (course.instructor == "Bob") ? "John" : "Bob")
    
    
    


struct Course: Identifiable 
    let id: String = UUID().uuidString
    var name: String
    var instructor: String

【讨论】:

还有一件事。注册您对 Apple 视频的评论 - 我同意 State 应该是私有的,但我面临一个问题,即即使从子视图中修改状态也不会更新状态。它是一种命中或失败,有时有效,有时无效。 如果您更新视图的状态,例如 100%,它将始终有效。但我认为您所说的关于 State 的问题与 NavigationView 相结合!或导航链接!您可能不知道,但那些 api 已经并且已经并且肯定会有这样的错误!如果您对 NavigationView 有此类问题,我不会感到惊讶。 哈哈哈,我遇到了臭名昭著的 14.4 或 14.5 NavigationView 关闭自身问题。但是,我认为我遇到了另一个问题,即我的状态没有更新。根据 Apple 的文档——You should only access a state property from inside the view’s body, or from methods called by it. 和我的一些方法正在从视图未直接调用的方法访问状态。

以上是关于SwiftUI - 使用 ForEach 时,您应该在子视图中使用 `@State var` 还是 `let`的主要内容,如果未能解决你的问题,请参考以下文章

从 ForEach 中引用变量时,SwiftUI“无法推断通用参数'数据'”

使用 ForEach 中的 SwiftUI 3.0 .swipeActions,您如何在将 ForEach 的输入参数传递给该视图时让一个动作转到另一个视图?

ForEach 完成时执行代码 - swiftUI

SwiftUI - 在 ForEach 中获取 TextField 数组

SwiftUI 列表未使用 ForEach 正确更新

使用 ForEach 插入的 SwiftUI 视图未随动画更新