大家好!作为 Swift 开发者,我们每天都在与数据打交道,而 JSON 与模型之间的转换无疑是家常便饭。苹果为我们提供了 Codable 协议,它在很多情况下表现出色,但随着业务逻辑变得复杂,我们常常会陷入编写大量样板代码的泥潭:手动定义 CodingKeys、实现 init(from:) 和 encode(to:)、处理嵌套结构、应对不同的命名风格、解析各种日期格式…… 这些繁琐的任务不仅耗时,还容易出错。
有没有更优雅、更高效的方式来处理 Swift 中的 Codable 呢?
答案是肯定的!随着 Swift 5.9+ 引入的 Swift Macros,代码生成的可能性被大大扩展。今天,我向大家介绍一款基于 Swift Macros 构建的框架 —— ReerCodable!
ReerCodable (github.com/reers/ReerC…) 旨在通过声明式的注解,彻底简化 Codable 的使用体验,让你告别繁琐的样板代码,专注于业务逻辑本身。
实际应用示例
让我们通过一个实际的例子来看看 ReerCodable 如何简化开发工作。假设我们有一个复杂的 API 响应:
{
"code": 0,
"data": {
"user_info": {
"user_name": "phoenix",
"birth_date": "1990-01-01T00:00:00Z",
"location": {
"city": "北京",
"country": "中国"
},
"height_in_meters": 1.85,
"is_vip": true,
"tags": ["tech", null, "swift"],
"last_login": 1731585275944
}
}
}
使用 ReerCodable,我们可以这样定义模型:
@Codable
struct ApiResponse {
var code: Int
@CodingKey("data.user_info")
var userInfo: UserInfo
}
@Codable
@SnakeCase
struct UserInfo {
var userName: String
@DateCoding(.iso8601)
var birthDate: Date
@CodingKey("location.city")
var city: String
@CodingKey("location.country")
var country: String
@CustomCoding<Double>(
decode: { return try $0.value(forKeys: "height_in_meters") * 100.0 },
encode: { try $0.set($1 / 100.0, forKey: "height_in_meters") }
)
var heightInCentimeters: Double
var isVip: Bool
@CompactDecoding
var tags: [String]
@DateCoding(.millisecondsSince1970)
var lastLogin: Date
}
// 使用方式
do {
// 原始的方式不变
let resp = try JSONDecoder().decode(ApiResponse.self, from: jsonString..data(using: .utf8)!)
// ReerCodable 提供的便捷方法
let response = try ApiResponse.decode(from: jsonString)
print("用户名: (response.userInfo.userName)")
print("出生日期: (response.userInfo.birthDate)")
print("身高(厘米): (response.userInfo.heightInCentimeters)")
} catch {
print("解析失败: (error)")
}
原生 Codable 的那些“痛”
在我们深入了解 ReerCodable 的魅力之前,先来回顾一下使用原生 Codable 时可能遇到的常见痛点:
- 手动
CodingKeys: 当 JSON key 与属性名不一致时,需要手动编写CodingKeys枚举,只修改一个, 其他所有属性都要写, 属性少还好,一旦多了,简直是噩梦。 - 嵌套 Key: 处理深层嵌套的 JSON 数据,需要定义多个中间结构体或手动编写解码逻辑。
- 命名风格转换: 后端返回 snake_case 或 kebab-case,而 Swift 推荐 camelCase,需要手动映射。
- 复杂的解码逻辑: 如需自定义解码(类型转换、数据修复等),就得实现
init(from:)。 - 默认值处理: 非 Optional 属性在 JSON 中缺失时,即使有默认值也会抛出
keyNotFound异常。Optional 枚举缺失也会导致解码失败。 - 忽略属性: 某些属性不需要参与编解码,需要手动在
CodingKeys或实现中处理。 - 日期格式多样: 时间戳、ISO8601、自定义格式…… 需要为
JSONDecoder配置不同的dateDecodingStrategy或手动处理。 - 集合中的
null: 数组或字典中包含null值时,若对应类型非 Optional,会导致解码失败。 - 继承: 父类的属性无法在子类的
Codable实现中自动处理。 - 枚举处理: 关联值枚举或需要匹配多种原始值的枚举,原生
Codable支持有限。
社区现状
Swift 社区为了解决 JSON 序列化的问题,涌现出了许多优秀的第三方框架。了解它们的设计哲学和优缺点,能帮助我们更好地理解为什么基于 Swift Macros 的方案是当下更优的选择。
1. 基于自定义协议的框架
ObjectMapper
ObjectMapper 是最早的一批 Swift JSON 解析库之一,它基于自定义的 Mappable 协议:
class User: Mappable {
var name: String?
var age: Int?
required init?(map: Map) {}
func mapping(map: Map) {
name <- map["user_name"]
age <- map["user_age"]
}
}
特点:
- 不依赖 Swift 的原生 Codable
- 不依赖反射机制
- 自定义操作符
<-使映射代码简洁 - 需要手动编写映射关系
- 支持嵌套映射和自定义转换
ObjectMapper 的优点是代码相对简洁,且不依赖 Swift 的内部实现细节,但缺点是需要手动编写映射代码,并且与 Swift 的原生序列化机制不兼容。
2. 基于运行时反射的框架
HandyJSON 和 KakaJSON
这两个框架采用了相似的实现原理,都是通过运行时反射获取类型信息:
struct User: HandyJSON {
var name: String?
var age: Int?
}
// 使用
let user = User.deserialize(from: jsonString)
特点:
- 通过底层运行时反射获取类型元数据
- 直接操作内存进行属性赋值
- 几乎无需编写额外代码
- 性能较高
这类框架的主要问题是强依赖 Swift 的内部实现细节和元数据结构(Metadata),随着 Swift 版本升级,容易出现不兼容问题或崩溃。它们实现了"零代码"的理想,但牺牲了稳定性和安全性。
3. 基于属性包装器(Property Wrapper)的框架
ExCodable 和 BetterCodable
这些框架利用 Swift 5.1 引入的属性包装器特性,为 Codable 提供扩展:
struct User: Codable {
@CustomKey("user_name")
var name: String
@DefaultValue(33)
var age: Int
}
特点:
- 基于 Swift 的原生 Codable
- 使用属性包装器简化常见编解码任务
- 无需手动编写 CodingKeys 和 Codable 实现
- 类型安全,编译时检查
属性包装器方案相比前两类有明显的优势:它既保持了与 Swift 原生 Codable 的兼容性,又简化了代码编写。但 PropertyWrapper 能力有限, 有些复杂的封装设计做不到.
4. 基于宏(Macro)的框架
CodableWrapper、CodableWrappers 和 MetaCodable,以及本文的 ReerCodable
这些框架利用 Swift 5.9 引入的宏特性,在编译时生成 Codable 的实现代码:
@Codable
struct User {
@CodingKey("user_name")
var name: String
var age: Int = 33
}
特点:
- 基于 Swift 的原生 Codable
- 声明式语法,直观易懂
- 高度灵活,支持复杂的编解码逻辑
- 可以在类型级别应用宏
宏方案结合了所有前述方案的优点,同时避免了它们的缺点:它基于原生 Codable,保持类型安全;它支持声明式语法,代码简洁;它在编译时生成代码,没有运行时性能损失;它能够处理复杂场景,适应性强。
为什么 Macro 是最优雅的解决方案?
在所有这些框架中,基于宏的方案(如 ReerCodable)提供了最优雅的解决方案,原因如下:
- 与原生 Codable 无缝集成:生成的代码与手写的 Codable 实现完全相同,可以与其他使用 Codable 的 API 完美配合。对于现代三方框架如 Alamofire, GRDB 等都与 Codable 互相兼容..
- 使用 Codable 可以支持三方编解码器: 如果不想使用 Foudation提供的解码器, 可以使用第三方库
- 声明式语法:通过注解方式声明序列化需求,代码简洁直观,意图明确。
- 类型安全:所有操作都在编译时进行类型检查,避免运行时错误。
- 高度灵活:可以处理各种复杂场景,如嵌套结构、自定义转换、条件编解码等。
- 维护性好:宏生成的代码是可预测的,而且不依赖于 Swift 的内部实现细节,随着 Swift 版本更新不会出现兼容性问题。
- 可调试性强:可以查看宏展开后的代码,便于理解和调试。
- 可扩展性:可以组合使用不同的宏,构建复杂的编解码逻辑。
- JSON 以外的格式支持:对于任何支持 Encoder 或 Decoder 的编解码器都通用,不只支持 JSON 格式,比如可以支持 PropertyListDecoder,YAMLDecoder,TOONEncoder 等
ReerCodable 登场:化繁为简的魔法
ReerCodable 利用 Swift Macros 的强大能力,让你只需在类型或属性前添加简单的注解,就能自动生成高效、健壮的 Codable 实现。核心就是 @Codable 宏,它会与其他 ReerCodable 提供的宏协同工作,生成最终的编解码逻辑。框架接入支持 Cocoapods, SwiftPackageManager。
代码实现上参考了优秀的 winddpan/CodableWrapper、GottaGetSwifty/CodableWrappers 和 MetaCodable,相对它们 ReerCodable 有更丰富的 feature 或更简洁的使用。
让我们来看看 ReerCodable 如何优雅地解决上述痛点:
1. 自定义 CodingKey
通过 @CodingKey 可以为属性指定自定义 key,无需手动编写 CodingKeys 枚举:
| ReerCodable | Codable |
|---|---|
|
|
2. 嵌套 CodingKey
支持通过点语法表示嵌套的 key path:
@Codable
struct User {
@CodingKey("other_info.weight")
var weight: Double
@CodingKey("location.city")
var city: String
}
3. 多键解码
可以指定多个 key 用于解码,系统会按顺序尝试解码直到成功:
@Codable
struct User {
@CodingKey("name", "username", "nick_name")
var name: String
}
4. 命名转换
支持多种命名风格转换,可以应用在类型或单个属性上:
@Codable
@SnakeCase
struct Person {
var firstName: String // 从 "first_name" 解码, 或编码为 "first_name"
@KebabCase
var lastName: String // 从 "last-name" 解码, 或编码为 "last-name"
}
5. 自定义编解码容器
使用 @CodingContainer 自定义编解码时的容器路径, 通常用于JSON嵌套较多, 但 model 声明 想直接 match 子层级结构:
| ReerCodable | JSON |
|---|---|
|
|
6. 编码专用 key
可以为编码过程指定不同的键名, 由于 @CodingKey 可能有多个参数, 再加上可以使用 @SnakeCase, KebabCase 等, 解码可能使用多个 key, 那编码时会采用第一个 key, 也可以通过 @EncodingKey 来指定 key
@Codable
struct User {
@CodingKey("user_name") // 解码使用 "user_name", "name"
@EncodingKey("name") // 编码使用 "name"
var name: String
}
7. 默认值支持
解码失败时可以使用默认值, 原生 Codable 针对非 Optional 属性, 会在没有解析到正确值时抛出异常导致整个 model 解码失败, 即使已经设置了初始值, 或者即使是 Optional 类型的枚举
@Codable
struct User {
var age: Int = 33
var name: String = "phoenix"
// 若 JSON 中 gender 字段不是 `male` 或 `female`, 原生 Codable 会抛出异常, ReerCodable 不会, 会设置其为 nil, 如 {"gender": "other"}, 可能出现在客户端定义了枚举, 但服务端新增了字段的业务场景
var gender: Gender?
}
@Codable
enum Gender: String {
case male, female
}
如果需要更精细地控制默认值, 可以使用 @DecodingDefault, @EncodingDefault, @CodingDefault:
@Decodable
struct Flags {
@DecodingDefault(false)
var isEnabled: Bool
}
@Encodable
struct Payload {
@EncodingDefault("anonymous")
var nickname: String?
}
@Codable
struct Preferences {
@CodingDefault([String]())
var tags: [String]?
}
@DecodingDefault 会在解码失败或缺失时使用提供的表达式, @EncodingDefault 会在编码 nil 可选值时使用该表达式, 而 @CodingDefault 同时具备前两者的行为。
8. 忽略属性
使用 @CodingIgnored 在编解码过程中忽略特定属性. 在解码过程中对于非 Optional 属性要有一个默认值才能满足 Swift 初始化的要求, ReerCodable 对基本数据类型和集合类型会自动生成默认值, 如果是其他自定义类型, 则需用用户提供默认值.
@Codable
struct User {
var name: String
@CodingIgnored
var ignore: Set<String>
}
9. Base64 编解码
自动处理 base64 字符串与 Data, [UInt8] 类型的转换:
@Codable
struct User {
@Base64Coding
var avatar: Data
@Base64Coding
var voice: [UInt8]
}
10. 集合类型解码优化
使用 @CompactDecoding 在解码数组时自动过滤 null 值, 与 compactMap 是相同的意思:
@Codable
struct User {
@CompactDecoding
var tags: [String] // ["a", null, "b"] 将被解码为 ["a", "b"]
}
同时, Dictionary 和 Set 也支持使用 @CompactDecoding 来优化
11. 日期编解码
支持多种日期格式的编解码:
| ReerCodable | JSON |
|---|---|
|
|
ISO8601 选项
如需更精细地控制 ISO8601 编码格式,可使用 .iso8601WithOptions 指定精度和时区:
@Codable
struct Event {
// 默认:秒精度,UTC 时区 -> "2024-01-15T12:00:00Z"
@DateCoding(.iso8601WithOptions())
var createdAt: Date
// 毫秒精度(3位小数) -> "2024-01-15T12:00:00.123Z"
@DateCoding(.iso8601WithOptions(precision: .milliseconds))
var timestamp: Date
// 微秒精度(6位小数) -> "2024-01-15T12:00:00.123456Z"
@DateCoding(.iso8601WithOptions(precision: .microseconds))
var preciseTime: Date
// 本地时区 -> "2024-01-15T20:00:00+08:00"
@DateCoding(.iso8601WithOptions(timeZone: .local))
var localTime: Date
// 固定时区偏移 -> "2024-01-15T20:00:00.123+08:00"
@DateCoding(.iso8601WithOptions(precision: .milliseconds, timeZone: .offsetHours(8)))
var beijingTime: Date
// 通过标识符指定时区 -> "2024-01-15T21:00:00+09:00"
@DateCoding(.iso8601WithOptions(timeZone: .identifier("Asia/Tokyo")))
var tokyoTime: Date
}
可用选项:
| DatePrecision | 说明 | 示例 |
|---|---|---|
.seconds | 无小数秒(默认) | "12:00:00" |
.milliseconds | 3位小数 | "12:00:00.123" |
.microseconds | 6位小数 | "12:00:00.123456" |
| TimeZoneStyle | 说明 | 示例 |
|---|---|---|
.utc | UTC 时区(默认) | "Z" |
.local | 设备本地时区 | "+08:00" |
.offsetHours(Int) | 固定小时偏移(-12 到 +14) | "+08:00" |
.offsetSeconds(Int) | 固定秒数偏移 | "+05:30" |
.identifier(String) | 时区标识符 | "Asia/Tokyo" → "+09:00" |
注意: 当时区偏移为 0 时,输出遵循 RFC 3339 标准使用
"Z"后缀(与 Go、Apple、JavaScript、Java 行为一致)。
12. 自定义编解码逻辑
通过 @CustomCoding 实现自定义的编解码逻辑. 自定义编解码有两种方式:
- 通过闭包, 以
decoder: Decoder,encoder: Encoder为参数来实现自定义逻辑:
@Codable
struct User {
@CustomCoding<Double>(
decode: { return try $0.value(forKeys: "height_in_meters") * 100.0 },
encode: { try $0.set($1 / 100.0, forKey: "height_in_meters") }
)
var heightInCentimeters: Double
}
- 通过一个实现
CodingCustomizable协议的自定义类型来实现自定义逻辑:
// 1st 2nd 3rd 4th 5th -> 1 2 3 4 5
struct RankTransformer: CodingCustomizable {
typealias Value = UInt
static func decode(by decoder: any Decoder, keys: [String]) throws -> UInt {
var temp: String = try decoder.value(forKeys: keys)
temp.removeLast(2)
return UInt(temp) ?? 0
}
static func encode(by encoder: Encoder, key: String, value: Value) throws {
try encoder.set(value, forKey: key)
}
}
@Codable
struct HundredMeterRace {
@CustomCoding(RankTransformer.self)
var rank: UInt
}
自定义实现过程中, 框架提供的方法也可以使编解码更加方便:
public extension Decoder {
func value<Value: Decodable>(forKeys keys: String...) throws -> Value {
let container = try container(keyedBy: AnyCodingKey.self)
return try container.decode(type: Value.self, keys: keys)
}
}
public extension Encoder {
func set<Value: Encodable>(_ value: Value, forKey key: String, treatDotAsNested: Bool = true) throws {
var container = container(keyedBy: AnyCodingKey.self)
try container.encode(value: value, key: key, treatDotAsNested: treatDotAsNested)
}
}
13. 继承支持
使用 @InheritedCodable 更好地支持子类的编解码. 原生 Codable 无法解析子类属性, 即使 JSON 中存在该值, 需要手动实现 init(from decoder: Decoder) throws
@Codable
class Animal {
var name: String
}
@InheritedCodable
class Cat: Animal {
var color: String
}
14. 枚举支持
为枚举提供丰富的编解码能力:
- 对基本枚举类型, 以及 RawValue 枚举支持
@Codable
struct User {
let gender: Gender
let rawInt: RawInt
let rawDouble: RawDouble
let rawDouble2: RawDouble2
let rawString: RawString
}
@Codable
enum Gender {
case male, female
}
@Codable
enum RawInt: Int {
case one = 1, two, three, other = 100
}
@Codable
enum RawDouble: Double {
case one, two, three, other = 100.0
}
@Codable
enum RawDouble2: Double {
case one = 1.1, two = 2.2, three = 3.3, other = 4.4
}
@Codable
enum RawString: String {
case one, two, three, other = "helloworld"
}
- 支持使用
CodingCase(match: ....)来匹配多个值或 range
@Codable
enum Phone: Codable {
@CodingCase(match: .bool(true), .int(10), .string("iphone"), .intRange(22...30))
case iPhone
@CodingCase(match: .int(12), .string("MI"), .string("xiaomi"), .doubleRange(50...60))
case xiaomi
@CodingCase(match: .bool(false), .string("oppo"), .stringRange("o"..."q"))
case oppo
}
- 对于有关联值的枚举, 支持通用
CaseValue来匹配关联值, 使用.label()来声明有标签的关联值的匹配逻辑, 使用.index()来声明没有标签的的关联值的匹配逻辑.ReerCodable支持两种JSON 格式的枚举匹配- 第一种是也是原生
Codable支持的, 即枚举值和其关联值是父子级的结构:
@Codable enum Video: Codable { /// { /// "YOUTUBE": { /// "id": "ujOc3a7Hav0", /// "_1": 44.5 /// } /// } @CodingCase(match: .string("youtube"), .string("YOUTUBE")) case youTube /// { /// "vimeo": { /// "ID": "234961067", /// "minutes": 999999 /// } /// } @CodingCase( match: .string("vimeo"), values: [.label("id", keys: "ID", "Id"), .index(2, keys: "minutes")] ) case vimeo(id: String, duration: TimeInterval = 33, Int) /// { /// "tiktok": { /// "url": "https://example.com/video.mp4", /// "tag": "Art" /// } /// } @CodingCase( match: .string("tiktok"), values: [.label("url", keys: "url")] ) case tiktok(url: URL, tag: String?) }- 第二种是枚举值和其关联值同级或自定义匹配的结构, 使用带有 key path 的 CaseMatcher 进行自定义路径值的匹配
@Codable enum Video1: Codable { /// { /// "type": { /// "middle": "youtube" /// } /// } @CodingCase(match: .string("youtube", at: "type.middle")) case youTube /// { /// "type": "vimeo", /// "ID": "234961067", /// "minutes": 999999 /// } @CodingCase( match: .string("vimeo", at: "type"), values: [.label("id", keys: "ID", "Id"), .index(2, keys: "minutes")] ) case vimeo(id: String, duration: TimeInterval = 33, Int) /// { /// "type": "tiktok", /// "media": "https://example.com/video.mp4", /// "tag": "Art" /// } @CodingCase( match: .string("tiktok", at: "type"), values: [.label("url", keys: "media")] ) case tiktok(url: URL, tag: String?) } - 第一种是也是原生
15. 生命周期回调
支持编解码的生命周期回调:
@Codable
class User {
var age: Int
func didDecode(from decoder: any Decoder) throws {
if age < 0 {
throw ReerCodableError(text: "Invalid age")
}
}
func willEncode(to encoder: any Encoder) throws {
// 在编码前进行处理
}
}
@Codable
struct Child: Equatable {
var name: String
mutating func didDecode(from decoder: any Decoder) throws {
name = "reer"
}
func willEncode(to encoder: any Encoder) throws {
print(name)
}
}
16. JSON 扩展支持
提供便捷的 JSON 字符串和字典转换方法:
let jsonString = "{\"name\": \"Tom\"}"
let user = try User.decode(from: jsonString)
let dict: [String: Any] = ["name": "Tom"]
let user2 = try User.decode(from: dict)
17. 基本类型转换
通过 @FlexibleType 启用基本数据类型之间的自动转换。可以应用在单个属性或整个类型上:
@Codable
struct User {
@FlexibleType
@CodingKey("is_vip")
var isVIP: Bool // 可以从 "1"、1、"true"、"yes" 解码为 true
@FlexibleType
@CodingKey("score")
var score: Double // 可以从 "100" 或 100 解码为 100.0
}
@Codable
@FlexibleType
struct Settings {
// 该类型的所有属性都将支持灵活类型转换
var isEnabled: Bool // 可以从数字或字符串解码
var count: Int // 可以从字符串解码
var amount: Double // 可以从字符串或整数解码
}
18. AnyCodable 支持
通过 AnyCodable 实现对 Any 类型的编解码:
@Codable
struct Response {
var data: AnyCodable // 可以存储任意类型的数据
var metadata: [String: AnyCodable] // 相当于[String: Any]类型
}
语法糖访问
AnyCodable 提供了便捷的下标语法糖,支持链式访问嵌套的 JSON 结构:
let json = AnyCodable([
"users": [
["name": "Alice", "age": 25],
["name": "Bob", "age": 30]
],
"total": 2
])
// 链式访问嵌套结构
let firstName = json["users"][0]["name"].string // "Alice"
let secondAge = json["users"][1]["age"].int // 30
let total = json["total"].int // 2
// 安全访问不存在的路径,返回 null 而不会崩溃
let invalid = json["users"][5]["name"].isNull // true
let missing = json["nonexistent"].isNull // true
类型转换属性
AnyCodable 提供了多种类型转换属性,方便快速获取对应类型的值:
| 属性 | 返回类型 | 说明 |
|---|---|---|
.bool | Bool? | 转换为布尔值 |
.int | Int? | 转换为整数 |
.uint | UInt? | 转换为无符号整数 |
.double | Double? | 转换为双精度浮点数 |
.string | String? | 转换为字符串 |
.array | [Any]? | 转换为数组 |
.dict | [String: Any]? | 转换为字典 |
.dictArray | [[String: Any]]? | 转换为字典数组 |
.isNull | Bool | 检查是否为 null |
.data | Data? | 转换为 JSON Data |
let json = AnyCodable([
"name": "phoenix",
"age": 33,
"isVIP": true,
"score": 99.5,
"tags": ["swift", "ios"],
"address": ["city": "Beijing", "country": "China"]
])
// 使用类型转换属性
let name = json["name"].string // "phoenix"
let age = json["age"].int // 33
let isVIP = json["isVIP"].bool // true
let score = json["score"].double // 99.5
let tags = json["tags"].array // ["swift", "ios"]
let address = json["address"].dict // ["city": "Beijing", "country": "China"]
// 创建和检查 null 值
let nullValue = AnyCodable.null
print(nullValue.isNull) // true
19. 生成默认实例
@Codable
@DefaultInstance
struct ImageModel {
var url: URL
}
@Codable
@DefaultInstance
struct User5 {
let name: String
var age: Int = 22
var uInt: UInt = 3
var data: Data
var date: Date
var decimal: Decimal = 8
var uuid: UUID
var avatar: ImageModel
var optional: String? = "123"
var optional2: String?
}
会生成以下实例
static let `default` = User5(
name: "",
age: 22,
uInt: 3,
data: Data(),
date: Date(),
decimal: 8,
uuid: UUID(),
avatar: ImageModel.default,
optional: "123",
optional2: nil
)
⚠️注意: 泛型类型的属性不支持使用 @DefaultInstance
@Codable
struct NetResponse<Element: Codable> {
let data: Element?
let msg: String
private(set) var code: Int = 0
}
20. 生成 copy 方法
使用 Copyable 为模型生成 copy 方法
@Codable
@Copyable
public struct Model6 {
var name: String
let id: Int
var desc: String?
}
@Codable
@Copyable
class Model7<Element: Codable> {
var name: String
let id: Int
var desc: String?
var data: Element?
}
生成如下 copy 方法, 可以看到, 除了默认 copy, 还可以对部分属性进行更新
public func copy(
name: String? = nil,
id: Int? = nil,
desc: String? = nil
) -> Model6 {
return .init(
name: name ?? self.name,
id: id ?? self.id,
desc: desc ?? self.desc
)
}
func copy(
name: String? = nil,
id: Int? = nil,
desc: String? = nil,
data: Element? = nil
) -> Model7 {
return .init(
name: name ?? self.name,
id: id ?? self.id,
desc: desc ?? self.desc,
data: data ?? self.data
)
}
20. 单独使用 @Decodable 或 @Encodable
@Decodable
struct Item: Equatable {
let id: Int
}
@Encodable
struct User3: Equatable {
let name: String
}
21. 使用 @Flat 扁平化属性
将某个嵌套属性在编解码时“拍平”,使其字段与父类型位于同一层级进行编码/解码。
@Codable
struct User {
var name: String
var age: Int = 0
@Flat
var address: Address
}
@Codable
struct Address {
var country: String
var city: String
}
// 输入
let dict: [String: Any] = [
"name": "phoenix",
"age": 34,
"country": "China",
"city": "Beijing"
]
let model = try User.decoded(from: dict)
// model == User(name: "phoenix", age: 34, address: Address(country: "China", city: "Beijing"))
22. NSObject 子类支持
@Codable 完全兼容继承自 NSObject 的类,宏会自动检测并正确处理 super.init() 的调用:
// 直接继承 NSObject
@Codable
public class Message: NSObject {
let title: String
let content: String
}
// 继承自 NSObject 子类
@Codable
class Article: NSObject {
let title: String
var content: String = ""
}
@InheritedCodable
class NewsArticle: Article {
let source: String
var publishDate: String = ""
}
所有 @Codable 的特性(如 @CodingKey、@SnakeCase、@FlexibleType 等)都可以与 NSObject 子类一起使用。
以上示例展示了 ReerCodable 的主要特性,这些特性可以帮助开发者大大简化编解码过程,提高代码的可读性和可维护性。
关于性能
ReerCodable 理论上与原生 Codable 性能相同, 但早期 Foudation JSONDecoder 的性能并不好, 所以社区出现了以下框架
我新写了一个比 ZippyJSON, Foudation.JSONDecoder IkigaJSON 性能都更好的 JSONDecoder github.com/reers/ReerJ…
从 ZippyJSON 主页的描述来看, 苹果似乎在 iOS17+优化了解码性能, 并超越了 ZippyJSON
Note: JSONDecoder is faster than ZippyJSON for iOS 17+. The rest of this document describes the performance difference pre-iOS 17.
我使用 ReerCodable 对 model 和其属性进行了标注, 对比了 Foudation JSONDecoder, ZippyJSON, IkigaJSON 三者的解码性能
name time std iterations
-----------------------------------------------------------------------------
JSON解码性能对比.创建 Foundation JSONDecoder 83.000 ns ± 95.19 % 1000000
JSON解码性能对比.创建 IkigaJSONDecoder 41.000 ns ± 96.06 % 1000000
JSON解码性能对比.创建 ZippyJSONDecoder 41.000 ns ± 77.25 % 1000000
JSON解码性能对比.Foundation JSONDecoder - 标准数据 313791.000 ns ± 3.63 % 4416
JSON解码性能对比.IkigaJSONDecoder - 标准数据 377583.000 ns ± 6.30 % 3692
JSON解码性能对比.ZippyJSONDecoder - 标准数据 310792.000 ns ± 3.62 % 4395
JSON解码性能对比.Foundation JSONDecoder - 小数据集 88334.000 ns ± 4.35 % 15706
JSON解码性能对比.IkigaJSONDecoder - 小数据集 98333.000 ns ± 4.96 % 14095
JSON解码性能对比.ZippyJSONDecoder - 小数据集 87625.000 ns ± 5.34 % 15747
JSON解码性能对比.Foundation JSONDecoder - 大数据集 5537916.500 ns ± 1.61 % 252
JSON解码性能对比.IkigaJSONDecoder - 大数据集 6445166.000 ns ± 2.30 % 217
JSON解码性能对比.ZippyJSONDecoder - 大数据集 5376375.000 ns ± 1.68 % 259
JSON解码性能对比.Foundation JSONDecoder - 嵌套结构 9167.000 ns ± 8.38 % 149385
JSON解码性能对比.IkigaJSONDecoder - 嵌套结构 10375.000 ns ± 13.73 % 131397
JSON解码性能对比.ZippyJSONDecoder - 嵌套结构 8458.000 ns ± 10.45 % 161606
JSON解码性能对比.Foundation JSONDecoder - 数组解析 2562250.000 ns ± 2.08 % 542
JSON解码性能对比.IkigaJSONDecoder - 数组解析 3620500.000 ns ± 1.63 % 385
JSON解码性能对比.ZippyJSONDecoder - 数组解析 2503709.000 ns ± 1.94 % 555
由上可见, 目前 ZippyJSONDecoder 性能还是最好的, Foudation JSONDecoder 的性能与之相差无几, 整体比旧版本的 JSONDecoder 要好很多
另外, 原生的 Foudation.JSONDecoder 解码性能依然没有超过 Ananda, 详见 AnandaBenchmark, 由以下跑分数据可见, Ananda 的性能大概是 Foudation.JSONDecoder 的两倍, 因此如果对性能有非常高的要求可以考虑使用 Ananda, 它也使用 Swift 宏进行了一些便捷封装, 但通常来说原生 Codable 目前已没有性能问题.
name time std iterations
------------------------------------------------------------
Codable decoding 5125.000 ns ± 14.92 % 271764
Ananda decoding 2541.000 ns ± 38.26 % 541187
Ananda decoding with Macro 2541.000 ns ± 64.55 % 550339
总结
ReerCodable 通过一系列精心设计的 Swift Macros,极大地简化了 Codable 的使用,显著减少了样板代码,提高了开发效率和代码可读性。它不仅涵盖了原生 Codable 的大部分场景,还提供了更强大、更灵活的功能,如多 key 解码、命名转换、自定义容器、健壮的默认值处理、强大的枚举支持以及便捷的辅助工具等。
如果你还在为 Codable 的繁琐实现而烦恼,不妨试试 ReerCodable,相信它会给你带来惊喜!
GitHub 地址: github.com/reers/ReerC…
欢迎大家试用、Star、提 Issue 或 PR!让我们一起用更现代、更优雅的方式来编写 Swift 代码!
文章主要由 AI 生成, 具体以 github readme 为准