WWDC26 全新 SwiftData 的 .codable 宏选项:它不是泛型专用胶水

引子

在 WWDC26 中,SwiftData 新增了 @Attribute(.codable) 选项。它的任务很明确:

当 SwiftData 不知道如何把某个 Codable 类型转换成自己的持久化 schema 时,让该类型自行编码,再由 SwiftData 保存编码后的表示。

先纠正一个常见叫法:.codable 并不是 Swift 语言的"关键词",而是 SwiftData @Attribute 宏的一个选项。

swift 复制代码
@Attribute(.codable)
var value: SomeCodableType

它解决的不是"这个类型有没有泛型",而是"SwiftData 能不能原生理解并建模这个类型"。

到底 .codable 选项有什么玄妙之处又有哪些陷阱和坑呢?

让我们马上了解一下吧!;-)


SwiftData 平时是怎么存属性的?

对常见属性,SwiftData 会直接为它们创建可持久化、可查询的 schema。

swift 复制代码
import SwiftData

@Model
final class Book {
    var title: String
    var pageCount: Int
    var publishedAt: Date

    init(title: String, pageCount: Int, publishedAt: Date) {
        self.title = title
        self.pageCount = pageCount
        self.publishedAt = publishedAt
    }
}

String、常规整数、Date 等类型,SwiftData 都认识。它们像数据库里的常驻嘉宾,不需要 .codable 介绍自己。


简单的 Codable struct 为什么通常不必加?

你拥有的简单值类型,即使是结构体,只要遵守 Codable,通常可以直接作为模型属性。

swift 复制代码
import SwiftData

@Model
final class Trip {
    struct Location: Codable {
        var latitude: Double
        var longitude: Double
    }

    var name: String
    var location: Location?

    init(name: String, location: Location? = nil) {
        self.name = name
        self.location = location
    }
}

这里不应因为 Location 遵守 Codable 就急着写:

swift 复制代码
// 通常没有必要这样做
@Attribute(.codable)
var location: Location?

原因是:Location 是一个简单、由你控制的值类型。让 SwiftData直接建模,未来更有机会获得查询、排序、索引和迁移方面的能力。

可以把 .codable 想成真空包装:

方便保存,但打开包装前,SwiftData 不知道里面装的是经纬度还是昨晚的披萨。


泛型类型为什么常常需要 .codable?

看起来,Measurement<UnitTemperature>、Measurement<UnitLength> 这类类型都因为带泛型才需要 .codable:

swift 复制代码
import Foundation
import SwiftData

@Model
final class Experiment {
    @Attribute(.codable)
    var temperature: Measurement<UnitTemperature>

    @Attribute(.codable)
    var length: Measurement<UnitLength>

    init(
        temperature: Measurement<UnitTemperature>,
        length: Measurement<UnitLength>
    ) {
        self.temperature = temperature
        self.length = length
    }
}

但真正原因不是尖括号。

Measurement 是 Foundation 提供的复杂类型;SwiftData 无法可靠地将它拆解为稳定的原生 schema,例如"数值列 + 单位列",并处理其单位体系与内部实现。因此需要 .codable 明确告诉 SwiftData:

不用研究它的内部构造;请调用它的编码和解码能力。

换句话说,泛型只是这类类型常见的外表,不是 SwiftData 拒绝建模的理由。


那么,Int128 为什么也需要 .codable?

这正是最容易让人困惑的地方。

Int128 没有泛型,而且是 Swift 标准库中的整数类型,也遵守 Codable。但它仍然需要这样保存:

swift 复制代码
import SwiftData

@Model
final class TimingRecord {
    @Attribute(.codable)
    var duration: Int128

    init(duration: Int128) {
        self.duration = duration
    }
}

原因是,能编码不等于能被 SwiftData 原生映射为数据库列。

SwiftData 原生支持的是一组它知道如何映射、比较、排序和查询的属性类型。Int128 虽然是标量,但宽度为 128 位,超出了 SwiftData 当前原生整数属性映射的范围。于是 .codable 让 SwiftData 把它当作一个可编码的整体保存。

所以,Measurement<UnitTemperature> 和 Int128 的共同点不是"都很复杂"或"都带泛型",而是:

SwiftData 都无法将它们直接视为可原生建模的属性。


.codable 得到了什么,又失去了什么?

.codable 的好处是可以保存原本无法纳入 SwiftData schema 的 Codable 类型,例如系统框架或第三方 SDK 的类型。

WWDC26 官方示例使用了 MKMapItem.Identifier:

swift 复制代码
import MapKit
import SwiftData

@Model
final class Trip {
    var name: String

    @Attribute(.codable)
    var mapItemIdentifier: MKMapItem.Identifier?

    init(
        name: String,
        mapItemIdentifier: MKMapItem.Identifier? = nil
    ) {
        self.name = name
        self.mapItemIdentifier = mapItemIdentifier
    }
}

这是一个典型的合适场景:该类型来自 MapKit,你不能修改它,也不能把它改造成自己的 @Model;但它遵守 Codable。

代价则是,SwiftData 会把 .codable 属性视为不透明整体:

swift 复制代码
// 不要期待 SwiftData 能理解或查询 codable 属性的内部字段
@Attribute(.codable)
var location: Location

你不能依赖其内部内容进行:

  • #Predicate 过滤;
  • SortDescriptor 排序;
  • 建立索引;
  • 自动 schema migration;

例如,若要按纬度搜索地点,不能把经纬度藏进 .codable 的 Location 后再指望 SwiftData"透视"它。它不会读心,也不会拆包。


自己的类型:什么时候该用,什么时候不该用?

对于你自己定义的业务模型,优先选择能被 SwiftData理解的普通属性或独立模型。

swift 复制代码
import SwiftData

@Model
final class WeatherRecord {
    var city: String
    var temperatureCelsius: Double
    var recordedAt: Date

    init(city: String, temperatureCelsius: Double, recordedAt: Date) {
        self.city = city
        self.temperatureCelsius = temperatureCelsius
        self.recordedAt = recordedAt
    }
}

这种设计很朴素,但很有力量:

swift 复制代码
let predicate = #Predicate<WeatherRecord> {
    $0.temperatureCelsius > 30
}

相反,若把所有内容塞进一个 .codable 结构体,存储会很省心,查询却会很伤心。

对于外部类型、不可修改的 SDK 类型,或者确实无法被 SwiftData 原生建模的值,.codable 才是合适工具。


.codable 类型演进时要格外小心

SwiftData 不会检查 .codable 属性内部结构的变化。新增字段、删除字段或更改字段类型,不会自动产生 SwiftData migration。

因此,你的 Codable 实现要主动兼容旧数据。

swift 复制代码
struct RemotePayload: Codable {
    var source: String
    var revision: Int

    // 新字段提供默认值,让旧数据仍可被解码
    var displayName: String = ""
}

对于更复杂的变更,应自定义 init(from:),为旧版本数据提供明确的回退策略。否则,应用升级后可能不是"数据迁移",而是"数据失忆"。


选择清单

你的属性 建议
String、Bool、Date、Int64 等常规原生类型 不加 .codable
自己定义的简单 Codable struct 或原始值枚举 通常不加 .codable
经常要筛选、排序、建立索引的数据 拆为原生属性,不要藏进 .codable
Measurement 等无法由 SwiftData 原生建模的 Foundation 类型 加 .codable
MKMapItem.Identifier 等第三方或系统框架的 Codable 类型 加 .codable
Int128、UInt128 加 .codable,或在业务允许时改用 Int64

总结

.codable 不是"泛型修饰符",也不是"只要遵守 Codable 就该加"的标记。它是 SwiftData 的一条备用通道:

当一个类型本身会编码和解码,但 SwiftData 不会原生建模时,使用 @Attribute(.codable)。

因此:

  • 简单、自有、希望被查询的数据:让 SwiftData 正常理解它。
  • 外部、复杂、无法原生映射的数据:交给 .codable。
  • Int128:不是泛型,但也不在 SwiftData 的原生整数映射范围内,所以同样适用 .codable。

最后,感谢大家的观赏!

我是大熊猫侯佩,我们下次不见不散!8-)

相关推荐
这个DBA有点耶4 天前
MVCC深入:Read View、版本链与快照读——InnoDB并发控制的内核
数据库·mysql·架构
DBA_G4 天前
从地面到云霄:GBase数据库在民航三大场景的落地实践
数据库
自由能燃气设备4 天前
商用全预混低氮冷凝锅炉免费方案vs付费方案对比+选型避坑指南
大数据·数据库·人工智能
科创致远4 天前
科创致远 ESOP 系统核心效能与实战价值展示
大数据·数据库·人工智能·精益工程
2601_962218614 天前
万象生鲜系统称重自动多退少补算法解决生鲜非标品痛点
大数据·数据库·人工智能·python·算法
张洛闻Eren4 天前
k8s云原生【第十课】:水平 Pod 自动扩缩容
运维·数据库·云原生·kubernetes·github
于平安4 天前
MySQL-触发器
数据库·mysql
白远山4 天前
上海24小时自助健身房解决方案实战指南与经验分享
java·数据库·架构·需求分析
Omics Pro4 天前
斯坦福Nature+Science|广义虚拟细胞基础大模型
数据库·人工智能·算法·机器学习·自然语言处理
2603_965148114 天前
AI+API选品:下一代智能商务助手雏形已现
java·大数据·数据库·人工智能·数据挖掘