SwiftData删除条目后恢复首个条目编辑状态时ViewModel报错
问题背景
我正在开发一个SwiftUI应用,用于管理带有关联子项的条目列表,架构包含CustomsViewModel、MainViewModel和SubViewModel三个ViewModel处理数据,数据模型基于SwiftData构建。
崩溃现象
删除列表条目后,尝试将首个条目恢复到编辑状态时出现崩溃。崩溃发生在CustomsViewModel的startEditing方法中,执行subViewModel.subName = item.subModel.subName这一步时——尽管赋值前能正常打印该值,但应用在此处崩溃。删除操作本身成功,恢复流程也能正确识别目标条目,仅在更新SubViewModel子项详情时出错。
注:因应用设计、即时数据更新等原因,架构调整难度较大,希望在现有架构下解决问题。
核心代码
Main Model
import SwiftData import SwiftUI extension SchemaV1 { @Model public final class MainModel { @Attribute(.unique) public let id: UUID public var createdAt: Date = Date.now public var mainName: String = "Main Name" public var isSelected: Bool = false @Relationship(deleteRule: .cascade) public var subModel: SubModel init(createdAt: Date, mainName: String, isSelected: Bool, subModel: SubModel) { self.id = UUID() self.createdAt = createdAt self.mainName = mainName self.isSelected = isSelected self.subModel = subModel } } }
Sub Model
import SwiftData import SwiftUI extension SchemaV1 { @Model public final class SubModel { @Attribute(.unique) public let id: UUID = UUID() public var subName: String = "Sub Name" init(subName: String) { self.id = UUID() self.subName = subName } } }
CustomsViewModel
import SwiftData import SwiftUI class CustomsViewModel: ObservableObject { var modelContext: ModelContext @MainActor static let sharedInstance = CustomsViewModel(modelContext: swiftDataTestContainer.mainContext) private let mainViewModel = MainViewModel.sharedInstance private let subViewModel = SubViewModel.sharedInstance @Published var items: [MainModel] = [] init(modelContext: ModelContext) { self.modelContext = modelContext self.items = fetchData() } func fetchData() -> [MainModel] { do { var fetchDescriptor = FetchDescriptor<MainModel>() fetchDescriptor.sortBy = [SortDescriptor<MainModel>(\.createdAt, order: .forward)] let items = try modelContext.fetch(fetchDescriptor) return items } catch { fatalError(error.localizedDescription) } } func startEditing(_ item: MainModel) { DispatchQueue.main.async {[unowned self] in items.forEach { $0.isSelected = false } item.isSelected = true mainViewModel.mainName = item.mainName mainViewModel.isSelected = item.isSelected // 崩溃发生在此处 subViewModel.subName = item.subModel.subName } } func addNewItem() { let newItem = MainModel(createdAt: Date.now, mainName: mainViewModel.mainName, isSelected: mainViewModel.isSelected, subModel: SubModel(subName: subViewModel.subName)) insert(newItem) startEditing(newItem) } func insert(_ item: MainModel) { modelContext.insert(item) save() } func delete(_ item: MainModel) { guard items.count > 1 else { print("Cannot delete the last remaining item.") return } modelContext.delete(item) save() restoreSelectedEditingItem() } func save() { do { try modelContext.save() items = fetchData() } catch { print(error.localizedDescription) } } func restoreSelectedEditingItem() { if let selectedItem = items.first(where: { $0.isSelected }) { startEditing(selectedItem) } else if let firstItem = items.first { firstItem.isSelected = true startEditing(firstItem) } } }
MainViewModel
import SwiftData import SwiftUI class MainViewModel: ObservableObject { @MainActor static let sharedInstance = MainViewModel(modelContext: swiftDataTestContainer.mainContext) var items: [MainModel] = [] var modelContext: ModelContext init(modelContext: ModelContext) { self.modelContext = modelContext self.items = fetchData() } @Published var mainName: String = "" { didSet { updateMainName() } } @Published var isSelected: Bool = false { didSet { updateIsSelected() items = fetchData() } } private func updateMainName() { guard let selectedItem = items.first(where: { $0.isSelected }) else { return } selectedItem.mainName = mainName save() } private func updateIsSelected() { guard let selectedItem = items.first(where: { $0.isSelected }) else { return } selectedItem.isSelected = isSelected save() } func fetchData() -> [MainModel] { do { var fetchDescriptor = FetchDescriptor<MainModel>() fetchDescriptor.sortBy = [SortDescriptor<MainModel>(\.createdAt, order: .reverse)] let items = try modelContext.fetch(fetchDescriptor) return items } catch { fatalError(error.localizedDescription) } } private func save() { do { try modelContext.save() items = fetchData() } catch { print(error.localizedDescription) } } }
SubViewModel
import SwiftData import SwiftUI class SubViewModel: ObservableObject { @MainActor static let sharedInstance = SubViewModel(modelContext: swiftDataTestContainer.mainContext) var items: [MainModel] = [] var modelContext: ModelContext init(modelContext: ModelContext) { self.modelContext = modelContext self.items = fetchData() } @Published var subName: String = "" { didSet { updateSubName() } } private func updateSubName() { guard let selectedItem = items.first(where: { $0.isSelected }) else { return } selectedItem.subModel.subName = subName } func fetchData() -> [MainModel] { do { var fetchDescriptor = FetchDescriptor<MainModel>() fetchDescriptor.sortBy = [SortDescriptor<MainModel>(\.createdAt, order: .reverse)] let items = try modelContext.fetch(fetchDescriptor) return items } catch { fatalError(error.localizedDescription) } } private func save() { do { try modelContext.save() items = fetchData() } catch { print(error.localizedDescription) } } }
错误信息
@Relationship(deleteRule: .cascade) public var subModel: SubModel { @storageRestrictions(accesses: _$backingData, initializes: _subModel) init(initialValue) { _$backingData.setValue(forKey: \.subModel, to: initialValue) _subModel = _SwiftDataNoType() } get { _$observationRegistrar.access(self, keyPath: \.subModel) // << 崩溃点 return self.getValue(forKey: \.subModel) } set { _$observationRegistrar.withMutation(of: self, keyPath: \.subModel) { self.setValue(forKey: \.subModel, to: newValue) } } }
解决方案
崩溃的核心原因是:删除条目后,ViewModel中可能仍持有已被SwiftData标记为删除的对象引用,访问其关联属性subModel时触发了上下文访问错误。同时多个ViewModel各自维护的items数组存在数据不同步问题,导致操作的不是最新的有效对象。
以下是在现有架构下的修改方案:
1. 确保startEditing操作的是最新有效对象
修改CustomsViewModel的startEditing方法,先刷新本地items数组,找到与传入item同ID的最新实例,避免操作已被删除的对象引用:
func startEditing(_ item: MainModel) { DispatchQueue.main.async {[unowned self] in // 刷新数据源,确保拿到最新的有效对象 self.items = self.fetchData() guard let validItem = self.items.first(where: { $0.id == item.id }) else { print("目标条目已不存在") return } self.items.forEach { $0.isSelected = false } validItem.isSelected = true self.mainViewModel.mainName = validItem.mainName self.mainViewModel.isSelected = validItem.isSelected // 此时访问subModel是安全的 self.subViewModel.subName = validItem.subModel.subName } }
2. 同步所有ViewModel的数据源
在恢复编辑状态时,强制刷新MainViewModel和SubViewModel的items数组,确保所有ViewModel使用的是同一套最新数据:
func restoreSelectedEditingItem() { // 同步其他ViewModel的数据源 mainViewModel.items = mainViewModel.fetchData() subViewModel.items = subViewModel.fetchData() if let selectedItem = items.first(where: { $0.isSelected }) { startEditing(selectedItem) } else if let firstItem = items.first { firstItem.isSelected = true startEditing(firstItem) } }
3. 避免SubViewModel使用缓存的旧数据
修改SubViewModel的updateSubName方法,每次更新时重新获取最新的数据源,防止访问已删除的对象:
private func updateSubName() { // 每次更新都获取最新数据 let latestItems = fetchData() guard let selectedItem = latestItems.first(where: { $0.isSelected }) else { return } selectedItem.subModel.subName = subName save() }
这些修改不需要调整核心架构,仅通过同步数据源、确保操作有效对象的方式解决崩溃问题,同时保证即时数据更新的需求。
内容的提问来源于stack exchange,提问作者Hajime

