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

SwiftData删除条目后恢复首个条目编辑状态时ViewModel报错

SwiftData关联模型删除后恢复编辑崩溃问题解决

问题背景

我正在开发一个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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 21:10:56