SwiftData 2027:Codable 类型持久化、分组查询与数据存储观察

2026-07-14 38 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

SwiftData 在 2027 版中扩展了三个关键方向:通过 Codable 持久化自定义及第三方类型、为 SwiftUI 列表组织分区数据,以及通过 ResultsObserverHistoryObserver 观察数据存储变化。这些能力让模型不必被拆成大量基础字段,也让后台同步、审计和跨界面刷新有了更明确的入口。

Codable 让领域模型少做一层转换

过去遇到坐标、货币、第三方 SDK 值对象等类型时,常见做法是将其拆成若干 SwiftData 字段,或者额外维护序列化数据。新版本允许通过 Codable 持久化这类值,模型可以更贴近业务语义。

下面是一个可以改造进项目的最小示例。示例假设项目使用支持该能力的 2027 SDK;具体属性宏或约束名称应以对应测试版 SDK 文档为准。

import Foundation
import SwiftData

struct Money: Codable, Equatable, Sendable {
    var amountInCents: Int
    var currencyCode: String
}

@Model
final class Invoice {
    var number: String
    var customer: String
    var issuedAt: Date
    var total: Money

    init(number: String, customer: String, issuedAt: Date, total: Money) {
        self.number = number
        self.customer = customer
        self.issuedAt = issuedAt
        self.total = total
    }
}

@MainActor
func insertExampleInvoice(into context: ModelContext) throws {
    let invoice = Invoice(
        number: "INV-2027-001",
        customer: "Example Studio",
        issuedAt: .now,
        total: Money(amountInCents: 129_00, currencyCode: "CNY")
    )

    context.insert(invoice)
    try context.save()
}

这类设计尤其适合具有稳定编码格式的值对象。不过,Codable 不是免费的模式迁移机制。删除字段、改变枚举编码值或调整第三方类型的编码实现,都可能让旧数据无法解码。生产项目应固定编码结构,并为升级路径准备版本字段或自定义 init(from:)

第三方类型还存在另一个边界:如果类型的 Codable 实现由依赖库控制,升级依赖就可能改变持久化格式。更稳妥的方式通常是定义自己的存储 DTO:

import Foundation

struct StoredCoordinate: Codable, Equatable {
    let latitude: Double
    let longitude: Double

    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}

这样既能使用 Codable 持久化,又不会把数据库兼容性交给第三方包的发布节奏。

查询结果直接服务于 SwiftUI 分区列表

新版本还支持把数据组织成 SwiftUI 列表分区。价值不只是少写几行 Dictionary(grouping:):分区逻辑进入查询层后,排序、刷新和界面结构可以共享同一套规则。

以发票列表为例,可以按月份展示分区。由于来源摘要没有给出分区查询 API 的完整签名,下面使用当前 SwiftUI 可直接运行的分组方式展示界面结构;迁移到新 SDK 时,可以把 sections 替换为 SwiftData 提供的分区查询结果。

import SwiftUI
import SwiftData

struct InvoiceList: View {
    @Query(sort: \Invoice.issuedAt, order: .reverse)
    private var invoices: [Invoice]

    private var sections: [(month: Date, invoices: [Invoice])] {
        let calendar = Calendar.current
        let grouped = Dictionary(grouping: invoices) { invoice in
            let components = calendar.dateComponents(
                [.year, .month],
                from: invoice.issuedAt
            )
            return calendar.date(from: components)!
        }

        return grouped
            .map { (month: $0.key, invoices: $0.value) }
            .sorted { $0.month > $1.month }
    }

    var body: some View {
        List {
            ForEach(sections, id: \.month) { section in
                Section(section.month.formatted(.dateTime.year().month())) {
                    ForEach(section.invoices) { invoice in
                        VStack(alignment: .leading) {
                            Text(invoice.number)
                            Text(invoice.customer)
                                .foregroundStyle(.secondary)
                        }
                    }
                }
            }
        }
    }
}

手工分组适合验证界面,但数据量变大后会在每次视图更新时重新遍历结果。原生分区查询更值得用于长期实现,尤其是在分区键、排序和谓词能够由存储层处理时。需要特别检查的是:分区键是否稳定、分区内排序是否确定,以及空分区在删除最后一条记录后如何消失。

ResultsObserver 与 HistoryObserver 解决不同层次的问题

ResultsObserverHistoryObserver 都用于观察变化,但它们面向的需求并不相同。

  • ResultsObserver 更接近查询结果:适合关心某个结果集合是否增加、删除或更新,例如徽标计数、派生缓存和非 SwiftUI 控制器。
  • HistoryObserver 更接近存储历史:适合增量同步、审计记录、索引更新,以及处理其他进程或上下文写入的数据。

可以这样划分职责:界面继续优先使用 @Query 或分区查询;需要脱离视图生命周期观察具体集合时使用 ResultsObserver;需要消费可追踪的持久化变更时使用 HistoryObserver

以下是观察器接入时可以采用的伪项目骨架。它展示生命周期和处理边界,不承诺具体初始化参数;请按 2027 SDK 中两个观察器的实际签名替换标记位置。

import SwiftData

@MainActor
final class StoreChangeCoordinator {
    private var resultsObserver: ResultsObserver?
    private var historyObserver: HistoryObserver?

    func start(container: ModelContainer) {
        // 按 2027 SDK 的实际 API 构造观察器,并保存强引用。
        // resultsObserver = ResultsObserver(...)
        // historyObserver = HistoryObserver(...)
        // resultsObserver?.start()
        // historyObserver?.start()
    }

    func stop() {
        // 按 SDK 要求取消订阅,避免重复回调或延长上下文生命周期。
        // resultsObserver?.stop()
        // historyObserver?.stop()
        resultsObserver = nil
        historyObserver = nil
    }
}

观察回调中不要直接执行长时间网络请求。更可靠的流程是读取变化标识、写入本地任务队列,再由独立 worker 执行同步。还应保存 history token 或等价游标,确保应用重启后能够从上次位置继续,而不是重复扫描全部历史。

升级时应先验证的清单

采用这些能力时,建议把迁移拆成三个独立步骤,而不是一次同时替换模型、查询和同步链路。

  1. 先为 Codable 类型建立往返测试:写入旧版本样本,升级后读取,再验证重新编码的数据仍兼容。
  2. 将一个低风险列表迁移到分区查询,测量首屏时间、滚动性能和批量变更时的刷新行为。
  3. 为观察器增加幂等处理、取消订阅和断点续传测试,尤其关注应用重启及多上下文写入。
  4. 不要在持久化模型中直接依赖编码格式不稳定的第三方类型;使用自有 DTO 隔离变化。
  5. 确认观察器的线程或 actor 约束,避免在回调中跨上下文传递 SwiftData 模型实例。

这次升级的重点不是增加更多模型语法,而是让“值如何保存、结果如何展示、变化如何传播”形成更完整的链路。Codable 持久化能减少样板转换,分区查询能降低界面整理成本,观察器则补上后台工作流所需的变化信号。真正落地时,数据格式兼容性和观察任务的幂等性仍然需要应用自己负责。


相关推荐