SwiftUI中修改Task类属性后ListView列表不更新问题咨询
当前问题核心在于 Task 是引用类型,@Published 仅检测存储属性的值变化:当数组存储引用类型时,修改数组内元素的属性不会改变数组本身的引用地址,因此不会触发@Published的更新通知,视图自然不会刷新。
你修改id后列表能刷新的原因是:ForEach 依赖指定的 id 做元素唯一标识,id 变更后 SwiftUI 会判定原元素被删除、新元素被插入,触发整体列表重绘,但此时原来的 NavigationLink 对应的元素标识消失,处于打开状态的详情页会被系统自动关闭,属于框架预期行为。
方案1:将Task改为值类型(推荐)
把Task从类改成结构体,值类型的属性变更会触发数组整体的变更,@Published可以正常检测到变化:
struct Task: Identifiable { var id: String = UUID().uuidString var title: String var description: String init(title: String, description: String) { self.title = title self.description = description } }
对应的ViewModel修改遍历方式,因为值类型需要直接修改数组元素:
func randomTasks() { // 遍历下标修改值类型元素 for i in tasks.indices { tasks[i].title = randomTitleTask() tasks[i].description = randomDescriptionTask() } }
这个方案不需要修改视图层代码,且不会出现详情页自动关闭的问题,是最优解。
方案2:保留类的前提下让Task独立可观测
如果业务逻辑必须保留Task为类,让Task遵守ObservableObject协议,属性加@Published修饰,然后在列表子视图中用@ObservedObject修饰接收的Task实例:
首先修改Task定义:
class Task: ObservableObject, Identifiable { var id: String = UUID().uuidString @Published var title: String @Published var description: String init(title: String, description: String) { self.title = title self.description = description } }
然后抽离列表的行视图,添加观测声明:
struct TaskRowView: View { @ObservedObject var task: Task var body: some View { VStack { Text(task.title) Text(task.description) } } }
最后替换原ForEach中的视图:
ForEach(viewModel.tasks, id: \.id) { task in NavigationLink( destination: DetailView(task: task) ) { TaskRowView(task: task) } }
注意DetailView中如果需要展示实时变更的title和description,也要用@ObservedObject修饰传入的task参数。
手动触发刷新的补充说明
你之前调用objectWillChange.send()不生效,大概率是调用位置错误,正确的调用位置是在ViewModel修改元素之前主动发送通知:
func randomTasks() { objectWillChange.send() // 主动触发视图刷新通知 tasks.forEach { task in task.title = randomTitleTask() task.description = randomDescriptionTask() } }
这个方案虽然可以生效,但属于非常规的强制刷新手段,不推荐在正式代码中使用。
内容的提问来源于stack exchange,提问作者Francois

