You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何无法对@ObservedObject中的数组元素执行更新/删除操作?

问题:SwiftUI中@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 02:01:13