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;

例如,若要按纬度搜索地点,不能把经纬度藏进 .codableLocation 后再指望 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:),为旧版本数据提供明确的回退策略。否则,应用升级后可能不是"数据迁移",而是"数据失忆"。


选择清单

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

总结

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

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

因此:

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

最后,感谢大家的观赏!

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

相关推荐
童谣13 小时前
越华环保集团市级统筹申报数字化中台:政策校验与减排仿真一体化架构
数据库·架构
制造数据与AI践行者老蒋3 小时前
离谱!PromptTemplate 遇上 JSON,花括号直接引发解析战争
数据库·microsoft·json
中微极客3 小时前
用LangChain 0.3构建生产级RAG与Agent:从API集成到Streamlit部署
数据库·人工智能·langchain
吴声子夜歌3 小时前
MongoDB 4.x——并发优化
数据库·mongodb
不如语冰4 小时前
AI大模型入门-Python进阶-上下文管理与with语句
开发语言·数据结构·数据库·人工智能·pytorch·redis·python
大龄秃头程序员5 小时前
Swift 并发实战:从 Task 到 Actor 重入性,我掉进了“逻辑不一致”的坑
swift
LinXunFeng6 小时前
给 Flutter 插件加上 Swift Package Manager 支持
flutter·swift
l1t6 小时前
DeepSeek总结的DuckLake 架构深度剖析-2
数据库·架构
YOU OU7 小时前
Redis事务
数据库·redis·缓存