SwiftUI中EnvironmentObject关联的任务进度UI无法及时更新求助
Swift克隆任务UI刷新问题及struct改造报错解决
问题描述
开发Git GUI客户端时,实现仓库克隆功能并通过Popover查看所有克隆任务及进度,遇到两个问题:
- 点击Start按钮更新任务进度后,UI无法及时刷新,仅当新增任务时才会更新旧任务的UI(推测数据已更新)
- 将
CloneTaskModel从class改为struct后,出现报错:无法在不可变值上使用可变成员'start()'(原报错英文:Cannot use mutating member on immutable value: 'task' is a 'let' constant)
原Class版本代码
class CloneTaskModel: Identifiable, ObservableObject { var id: String = UUID().uuidString var title: String @Published var progress: CGFloat = 0 init(title: String) { self.title = title } func start() { self.progress = 50 } } class CloneTaskViewModel: ObservableObject { @Published var tasks: [CloneTaskModel] = [] func clone(title: String) { self.tasks.append(CloneTaskModel(title: title)) } func deleteCloneTask(indexSet: IndexSet) { self.tasks.remove(atOffsets: indexSet) } } struct ObservableObjectBootcamp: View { @StateObject var cloneTaskViewModel: CloneTaskViewModel = CloneTaskViewModel() var body: some View { NavigationSplitView { List { ForEach(0..<10, id: \.self) { item in HStack { Text("\(item)") Spacer() Button("Clone", action: { cloneTaskViewModel.clone(title: "Task \(item)") }) } } } } detail: { CloneTaskPopover() .environmentObject(cloneTaskViewModel) } } } struct CloneTaskPopover: View { @EnvironmentObject var cloneTaskViewModel: CloneTaskViewModel var body: some View { List { ForEach(cloneTaskViewModel.tasks) { task in HStack { Text(task.title) .font(.title) ProgressView(value: task.progress, total: 100) Button("Start") { task.start() } } } .onDelete(perform: cloneTaskViewModel.deleteCloneTask) } .navigationTitle("Clone Tasks") } }
修改后的Struct版本代码(报错版本)
struct CloneTaskModel: Identifiable { var id: String = UUID().uuidString var title: String var progress: CGFloat = 0 init(title: String) { self.title = title } mutating func start() { self.progress = 50 } }
问题分析与解决
1. Class版本UI不刷新的解决
问题原因
CloneTaskViewModel中的tasks数组通过@Published修饰,但数组内的CloneTaskModel对象属性更新时,数组本身的引用并未改变,@Published只会在数组的元素增删、整体替换时触发视图刷新,单个对象的属性变化无法触发通知。
修复代码
在ViewModel中添加更新任务进度的方法,手动触发视图刷新:
class CloneTaskViewModel: ObservableObject { @Published var tasks: [CloneTaskModel] = [] func clone(title: String) { self.tasks.append(CloneTaskModel(title: title)) } func deleteCloneTask(indexSet: IndexSet) { self.tasks.remove(atOffsets: indexSet) } // 新增:通过任务ID更新进度 func updateTaskProgress(for taskId: String) { guard let taskIndex = tasks.firstIndex(where: { $0.id == taskId }) else { return } tasks[taskIndex].progress = 50 // 手动发送变化通知,强制UI刷新 objectWillChange.send() } } // 修改Popover中的按钮点击逻辑 struct CloneTaskPopover: View { @EnvironmentObject var cloneTaskViewModel: CloneTaskViewModel var body: some View { List { ForEach(cloneTaskViewModel.tasks) { task in HStack { Text(task.title) .font(.title) ProgressView(value: task.progress, total: 100) Button("Start") { cloneTaskViewModel.updateTaskProgress(for: task.id) } } } .onDelete(perform: cloneTaskViewModel.deleteCloneTask) } .navigationTitle("Clone Tasks") } }
2. Struct版本报错的解决
问题原因
ForEach循环中取出的task是不可变的let常量,而struct的start()是mutating方法,无法直接在不可变实例上调用。同时struct是值类型,直接修改实例不会同步到数组中,需要通过ViewModel替换数组内的元素来触发@Published的刷新。
修复代码
struct CloneTaskModel: Identifiable { var id: String = UUID().uuidString var title: String var progress: CGFloat = 0 init(title: String) { self.title = title } mutating func start() { self.progress = 50 } } class CloneTaskViewModel: ObservableObject { @Published var tasks: [CloneTaskModel] = [] func clone(title: String) { self.tasks.append(CloneTaskModel(title: title)) } func deleteCloneTask(indexSet: IndexSet) { self.tasks.remove(atOffsets: indexSet) } // 新增:启动指定任务 func startTask(for taskId: String) { guard let taskIndex = tasks.firstIndex(where: { $0.id == taskId }) else { return } var updatedTask = tasks[taskIndex] updatedTask.start() // 替换数组中的元素,触发@Published刷新 tasks[taskIndex] = updatedTask } } // 修改Popover中的按钮点击逻辑 struct CloneTaskPopover: View { @EnvironmentObject var cloneTaskViewModel: CloneTaskViewModel var body: some View { List { ForEach(cloneTaskViewModel.tasks) { task in HStack { Text(task.title) .font(.title) ProgressView(value: task.progress, total: 100) Button("Start") { cloneTaskViewModel.startTask(for: task.id) } } } .onDelete(perform: cloneTaskViewModel.deleteCloneTask) } .navigationTitle("Clone Tasks") } }
总结
- Class版本:需通过ViewModel统一修改任务属性,并手动发送变化通知,确保UI感知到更新
- Struct版本:需通过ViewModel替换数组内的任务实例,利用值类型的特性触发
@Published的刷新,同时避免直接在循环中调用mutating方法
内容的提问来源于stack exchange,提问作者xieyuancode
相关产品推荐
相关产品推荐

