为何无法对@ObservedObject中的数组元素执行更新/删除操作?
我定义了Activity结构体,包含id(UUID类型)、name(String类型)、description(String类型)和timesCompleted(Int类型)。同时创建了标记为ObservableObject的Activities类,其中包含Activity结构体数组activityList。
在ContentView中,我将activities声明为@StateObject,并传递给ActivityDetailView,子视图中用@ObservedObject接收。但子视图里只能对activities.activityList执行append操作,尝试覆盖、更新或删除数组元素时,无报错但视图会立即崩溃返回ContentView。我需要更新现有元素,而非追加新副本。
相关代码
Activity结构体
struct Activity : Codable, Identifiable, Equatable { var id = UUID() var name: String var description: String var timesCompleted: Int }
Activities类
class Activities: ObservableObject { @Published var activityList = [Activity]() { didSet { if let encoded = try? JSONEncoder().encode(activityList) { UserDefaults.standard.set(encoded, forKey: "activityList") } } } init() { if let savedList = UserDefaults.standard.data(forKey: "activityList") { if let decodedList = try? JSONDecoder().decode([Activity].self, from: savedList) { activityList = decodedList return } } activityList = [] } init(activityList: [Activity]) { self.activityList = activityList } subscript(index: Int) -> Activity { get { assert(index < activityList.count, "Index out of range") return activityList[index] } set { assert(index < activityList.count, "Index out of range") activityList[index] = newValue } } }
ContentView
struct ContentView: View { @StateObject var activities = Activities() @State private var showingAddActivity = false var body: some View { NavigationView { List { ForEach(activities.activityList) { activity in NavigationLink { ActivityDetailView(activity: activity, activities: activities) } label: { Text(activity.name) } } } .navigationTitle("Habits") .toolbar { Button { showingAddActivity = true let _ = print("add activity") } label: { Image(systemName: "plus") } } } .sheet(isPresented: $showingAddActivity) { AddActivityView(activities: activities) } } }
ActivityDetailView
struct ActivityDetailView: View { @State private var timesCompleted = 0 let activity: Activity @ObservedObject var activities: Activities var body: some View { NavigationView { Form { Text("Activity: \(activity.name)") Text("Description: \(activity.description)") Stepper { Text("Times Completed: \(timesCompleted)") } onIncrement: { timesCompleted += 1 updateTimesCompleted() } onDecrement: { if timesCompleted > 0 { timesCompleted -= 1 updateTimesCompleted() } } } .navigationTitle("Activity Details") } } func updateTimesCompleted() { let newActivity = Activity(name: activity.name, description: activity.description, timesCompleted: timesCompleted) let _ = print("count: \(activities.activityList.count)") let index = activities.activityList.firstIndex(of: activity) let _ = print(index ?? -666) if let index = index { activities.activityList[index] = Activity(name: activity.name, description: activity.description, timesCompleted: timesCompleted) //activities.activityList.swapAt(index, activities.activityList.count - 1) //activities.activityList[index].incrementTimesCompleted() //activities.activityList.append(newActivity) //activities.activityList.remove(at: index) //activities.activityList.removeAll() //activities.activityList.append(newActivity) } } }
问题原因及解决办法
核心问题
崩溃的本质是:NavigationLink的目标视图依赖的原始Activity实例发生了变化。当你更新数组里的Activity元素时,传给ActivityDetailView的旧Activity实例和数组里的新实例不再是同一个对象,SwiftUI识别到这种依赖变化后,会直接销毁当前的ActivityDetailView并返回列表页。
修复步骤
1. 修改ActivityDetailView:用ID而非实例接收数据
不再直接接收整个Activity实例,改为接收Activity的ID,从activities中实时获取最新数据:
struct ActivityDetailView: View { let activityId: UUID @ObservedObject var activities: Activities // 实时获取最新的Activity实例 private var activity: Activity? { activities.activityList.first { $0.id == activityId } } // 用Activity的真实数据初始化timesCompleted @State private var timesCompleted: Int init(activityId: UUID, activities: Activities) { self.activityId = activityId self.activities = activities self._timesCompleted = State(initialValue: activities.activityList.first { $0.id == activityId }?.timesCompleted ?? 0) } var body: some View { NavigationView { Form { if let activity = activity { Text("Activity: \(activity.name)") Text("Description: \(activity.description)") Stepper { Text("Times Completed: \(timesCompleted)") } onIncrement: { timesCompleted += 1 updateTimesCompleted() } onDecrement: { if timesCompleted > 0 { timesCompleted -= 1 updateTimesCompleted() } } } else { Text("Activity not found") } } .navigationTitle("Activity Details") } } func updateTimesCompleted() { guard let index = activities.activityList.firstIndex(where: { $0.id == activityId }), let activity = activity else { return } // 保留原ID更新元素,确保实例身份稳定 activities.activityList[index] = Activity(id: activityId, name: activity.name, description: activity.description, timesCompleted: timesCompleted) } }
2. 修改ContentView的导航链接传递参数
把传递Activity实例改成传递ID:
ForEach(activities.activityList) { activity in NavigationLink { ActivityDetailView(activityId: activity.id, activities: activities) } label: { Text(activity.name) } }
3. 可选优化:给Activities类封装更新方法
在Activities类里添加专门的更新逻辑,让视图代码更简洁:
class Activities: ObservableObject { // ... 原有代码保持不变 ... func updateActivity(id: UUID, newTimesCompleted: Int) { guard let index = activityList.firstIndex(where: { $0.id == id }) else { return } var updatedActivity = activityList[index] updatedActivity.timesCompleted = newTimesCompleted activityList[index] = updatedActivity } }
此时ActivityDetailView的updateTimesCompleted可以简化为:
func updateTimesCompleted() { activities.updateActivity(id: activityId, newTimesCompleted: timesCompleted) }
修复原理
现在ActivityDetailView依赖的是稳定的Activity ID,而非易变的实例对象。当数组里的元素更新时,视图会通过ID获取最新的实例数据,不会因为实例变化被销毁,导航栈能保持稳定。
内容的提问来源于stack exchange,提问作者Legion

