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

Swift Data未保存对象异常更新数据库与视图问题及方案咨询

应用场景描述

现有一个TabView,不同标签页通过Swift Data加载数据,部分视图会修改数据的@Transient字段。其中一个标签页包含跳转到账户创建页面的NavigationLink,且已设置.isAutoSaving: false。

代码实现如下:

enum mode {
    case create, update
}

struct EditAccount: View {
    
    @Environment(\.dismiss) var dismiss
    @Environment(\.modelContext) var modelContext
    @Query var currencies: [Currency]
    @Bindable var account: Account
    var oldAccount: Account = Account()
    
    var mode: mode
    
    init(_ account: Account) {
        mode = .update
        self.oldAccount = account
        _account = .init(wrappedValue: account)
    }
    
    init(accountType: AccountType) {
        mode = .create
        _account = .init(wrappedValue: Account(
                id: UInt32.random(in: 10000..<10000000),
                type: accountType
            )
        )
    }
        
    var body: some View {
        Form {
            Section {
                TextField("Account name", text: $account.name)
                TextField("Budget", value: $account.budget, format: .number)
                    .keyboardType(.decimalPad)
            }
            Section {
                if mode == .create {
                    Picker("Currency", selection: $account.currency) {
                        ForEach(currencies) { currency in
                            Text(currency.isoCode)
                                .tag(currency as Currency?)
                        }
                    }
                }
            }
            Section {
                Button("Save") {
                    Task {
                        dismiss()
                        switch mode {
                        case .create:
                            await createAccount()
                        case .update:
                            await updateAccount()
                        }
                    }
                }
            }
            .frame(maxWidth: .infinity)
        }
        .navigationTitle(mode == .create ? "Create account" : "Update account")
    }

    func createAccount() async {
        do {
            let id = try await AccountAPI().CreateAccount(req: CreateAccountReq(...))
            account.id = id
            try modelContext.save()
        } catch {
            modelContext.rollback()
        }
    }
    
    func updateAccount() async {
        do {
            try await AccountAPI().UpdateAccount(req: UpdateAccountReq(
                id: account.id,
                accounting: oldAccount.accounting == account.accounting ? account.accounting : nil,
                // same code
            ))
            try modelContext.save()
        } catch {
            modelContext.rollback()
        }
    }
}

问题

  1. 在创建模式(Mode = .create)下修改新账户的任意内容时,其他标签页的视图更新函数会被触发(控制台有打印输出),怎么改才能避免这种情况?
  2. 创建新账户时选择货币后,还没点击Save按钮,这个未保存的账户就出现在其他标签页的账户列表里了,如何实现“不点击Save就不让新元素进入数据库”的需求?
  3. 每次修改金额或选择货币时,其他页面都会重新计算,导致界面卡顿,该怎么解决?
  4. 如何复制数据库对象修改前的状态,用来对比特定字段是否变化,从而实现PATCH请求的逻辑?

解决方案

问题1&2:避免创建模式下未保存数据干扰其他视图

核心问题是创建模式下直接将新Account对象加入了共享modelContext,即使未执行save,Swift Data的@Query也会追踪上下文内的所有对象(包括未持久化的)。解决思路是给创建模式单独使用临时ModelContext,直到点击Save再将对象迁移到共享上下文:

修改EditAccount的初始化逻辑和创建流程:

enum Mode { // 修正命名规范
    case create, update
}

struct EditAccount: View {
    
    @Environment(\.dismiss) var dismiss
    @Environment(\.modelContext) private var sharedModelContext
    @Query var currencies: [Currency]
    @Bindable private var account: Account
    private var oldAccount: Account?
    private let mode: Mode
    private let tempModelContext: ModelContext? // 临时上下文仅创建模式使用

    // 更新模式初始化
    init(_ account: Account) {
        mode = .update
        self.oldAccount = account
        self.tempModelContext = nil
        _account = .init(wrappedValue: account)
    }

    // 创建模式初始化,需要传入ModelContainer
    init(accountType: AccountType, modelContainer: ModelContainer) {
        mode = .create
        self.tempModelContext = ModelContext(modelContainer)
        // 创建临时账户并加入临时上下文
        let tempAccount = Account(
            id: UInt32.random(in: 10000..<10000000),
            type: accountType
        )
        tempModelContext.insert(tempAccount)
        _account = .init(wrappedValue: tempAccount)
    }
    
    var body: some View {
        // 原有body代码保持不变
    }

    func createAccount() async {
        do {
            let id = try await AccountAPI().CreateAccount(req: CreateAccountReq(...))
            account.id = id
            // 将临时账户迁移到共享上下文并保存
            guard let tempContext = tempModelContext else { return }
            sharedModelContext.insert(account)
            tempContext.delete(account) // 从临时上下文移除
            try sharedModelContext.save()
        } catch {
            tempModelContext?.rollback()
        }
    }
    
    // 更新模式代码保持不变
}

调用创建模式时传入ModelContainer:

// 父视图中调用
NavigationLink("创建账户") {
    EditAccount(accountType: .bank, modelContainer: modelContainer)
}

这样创建模式下的所有修改都隔离在临时上下文,不会触发共享上下文的@Query更新,也不会让未保存的账户出现在其他标签页列表中。

问题3:解决界面卡顿

卡顿根源是共享上下文内的对象修改会触发所有监听该对象的视图重绘,结合上述临时上下文方案已经解决创建模式的卡顿。针对更新模式,还可以做以下优化:

  • 给@Query添加.transaction修饰符,批量处理更新,减少频繁重绘:
@Query(...) var accounts: [Account]

var body: some View {
    List(accounts) { account in
        // 列表内容
    }
    .transaction { transaction in
        transaction.disablesAutosave = true // 和全局设置保持一致
    }
}
  • 使用@Observable结构体做数据中转,减少视图对Swift Data对象的直接依赖:
// 定义展示用的结构体
struct AccountDisplayModel: Observable {
    let id: UInt32
    var name: String
    var budget: Double

    init(from account: Account) {
        self.id = account.id
        self.name = account.name
        self.budget = account.budget
    }
}

// 列表视图中使用
ForEach(accounts.map(AccountDisplayModel.init)) { model in
    Text(model.name)
}

问题4:复制对象初始状态实现PATCH逻辑

直接赋值oldAccount = account是引用传递,修改account会同步改变oldAccount,需要实现深拷贝:

// 给Account添加拷贝方法
extension Account {
    func copy() -> Account {
        let copied = Account()
        copied.id = self.id
        copied.name = self.name
        copied.budget = self.budget
        copied.currency = self.currency
        copied.type = self.type
        // 复制所有需要对比的字段
        return copied
    }
}

在更新模式初始化器中使用拷贝:

init(_ account: Account) {
    mode = .update
    self.oldAccount = account.copy() // 深拷贝初始状态
    self.tempModelContext = nil
    _account = .init(wrappedValue: account)
}

之后在更新接口中对比字段生成PATCH请求:

func updateAccount() async {
    do {
        var req = UpdateAccountReq(id: account.id)
        if oldAccount?.name != account.name {
            req.name = account.name
        }
        if oldAccount?.budget != account.budget {
            req.budget = account.budget
        }
        // 其他字段同理
        try await AccountAPI().UpdateAccount(req: req)
        try sharedModelContext.save()
    } catch {
        sharedModelContext.rollback()
    }
}

内容的提问来源于stack exchange,提问作者bonavii

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 16:42:33