使用 Swift macros 对 Codable
进行扩展, 以声明式注解让序列化变得更简单!
@Codable
@SnakeCase
struct User {
@CodingKey("user_name")
var name: String
@KebabCase
@DateCoding(.iso8601)
var birthDate: Date
@CodingKey("location.city")
var city: 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 height: Double
}
ReerCodable 框架提供了一系列自定义宏,用于生成动态的 Codable 实现。该框架的核心是 @Codable() 宏,它可以在其他宏提供的数据标记下生成具体的实现(@Cdable
宏, 展开其他宏是没有响应的)
主要包含以下 feature:
- 通过
@CodingKey("key")
为每个属性声明自定义的CodingKey
值, 无需编写所有的CodingKey
值. - 支持通过字符串表达嵌套的
CodingKey
, 如@CodingKey("nested.key")
- 允许使用多个
CodingKey
来进行 Decode, 如@CodingKey("key1", "key2")
- 支持使用
@SnakeCase
,KebabCase
等来标记类型或属性来方便地实现命名转换 - 通过使用
@CodingContainer
自定义 Coding 时的嵌套容器 - 支持 Encode 时指定的
CodingKey
, 如EncodingKey("encode_key")
- 允许解码失败时使用默认值, 从而避免
keyNotFound
错误发生 - 允许使用
@CodingIgnored
在编解码过程中忽略特定属性 - 支持使用
@Base64Coding
自动对 base64 字符串和Data
[UInt8]
类型进行转换 - 在 Decode
Array
,Dictionary
,Set
时, 通过@CompactDecoding
可以忽略null
值, 而不是抛出错误 - 支持通过
@DateCoding
实现对Date
的各种编解码 - 支持通过
@CustomCoding
实现自定义编解码逻辑 - 通过使用
@InheritedCodable
对子类有更好的支持 - 对各类
enum
提供简单而丰富的编解码能力 - 支持通过
ReerCodableDelegate
来编解码生命周期, 如didDecode
,willEncode
- 提供扩展, 支持使用 JSON String,
Dictionary
,Array
直接作为参数进行编解码 - 支持
Bool
,String
,Double
,Int
,CGFloat
等基本数据类型互相转换 - 支持通过
AnyCodable
来实现对Any
的编解码, 如var dict = [String: AnyCodable]
XCode 16.0+
iOS 13.0+, macOS 10.15+, tvOS 13.0+, visionOS 1.0+, watchOS 6.0+
Swift 5.10+
swift-syntax 600.0.0+
Swift Package Manager
你可以使用 The Swift Package Manager 来安装 ReerCodable,请在你的 Package.swift
文件中添加正确的描述:
import PackageDescription
let package = Package(
name: "YOUR_PROJECT_NAME",
targets: [],
dependencies: [
.package(url: "https://github.com/reers/ReerCodable.git", from: "1.1.0")
]
)
接下来,将 ReerCodable
添加到您的 targets 依赖项中,如下所示:
.product(name: "ReerCodable", package: "ReerCodable"),
然后运行 swift package update
。
CocoaPods
由于 CocoaPods 不支持直接使用 Swift Macro, 可以将宏实现编译为二进制提供使用, 接入方式如下, 需要设置s.pod_target_xcconfig
来加载宏实现的二进制插件:
Pod::Spec.new do |s|
s.name = 'YourPod'
s.dependency 'ReerCodable', '1.1.0'
# 复制以下 config 到你的 pod
s.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '-Xfrontend -load-plugin-executable -Xfrontend ${PODS_ROOT}/ReerCodable/Sources/Resources/ReerCodableMacros#ReerCodableMacros'
}
end
或者, 如果不使用s.pod_target_xcconfig
和s.user_target_xcconfig
, 也可以在 podfile 中添加如下脚本统一处理:
post_install do |installer|
installer.pods_project.targets.each do |target|
rhea_dependency = target.dependencies.find { |d| ['ReerCodable'].include?(d.name) }
if rhea_dependency
puts "Adding ReerCodable Swift flags to target: #{target.name}"
target.build_configurations.each do |config|
swift_flags = config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)']
plugin_flag = '-Xfrontend -load-plugin-executable -Xfrontend ${PODS_ROOT}/ReerCodable/Sources/Resources/ReerCodableMacros#ReerCodableMacros'
unless swift_flags.join(' ').include?(plugin_flag)
swift_flags.concat(plugin_flag.split)
end
config.build_settings['OTHER_SWIFT_FLAGS'] = swift_flags
end
end
end
end
ReerCodable 通过声明式注解大大简化了 Swift 的序列化过程。以下是各个特性的详细使用示例:
通过 @CodingKey
可以为属性指定自定义 key,无需手动编写 CodingKeys
枚举:
ReerCodable | Codable |
---|---|
@Codable
struct User {
@CodingKey("user_name")
var name: String
@CodingKey("user_age")
var age: Int
var height: Double
} |
struct User: Codable {
var name: String
var age: Int
var height: Double
enum CodingKeys: String, CodingKey {
case name = "user_name"
case age = "user_age"
case height
}
} |
支持通过点语法表示嵌套的 key path:
@Codable
struct User {
@CodingKey("other_info.weight")
var weight: Double
@CodingKey("location.city")
var city: String
}
可以指定多个 key 用于解码,系统会按顺序尝试解码直到成功:
@Codable
struct User {
@CodingKey("name", "username", "nick_name")
var name: String
}
支持多种命名风格转换,可以应用在类型或单个属性上:
@Codable
@SnakeCase
struct Person {
var firstName: String // 从 "first_name" 解码, 或编码为 "first_name"
@KebabCase
var lastName: String // 从 "last-name" 解码, 或编码为 "last-name"
}
使用 @CodingContainer
自定义编解码时的容器路径, 通常用于根层级的 model 解析:
ReerCodable | JSON |
---|---|
@Codable
@CodingContainer("data.info")
struct UserInfo {
var name: String
var age: Int
} |
{
"code": 0,
"data": {
"info": {
"name": "phoenix",
"age": 33
}
}
} |
可以为编码过程指定不同的键名, 由于 @CodingKey
可能有多个参数, 再加上可以使用 @SnakeCase
, KebabCase
等, 解码可能使用多个 key, 那编码时会采用第一个 key, 也可以通过 @EncodingKey
来指定 key
@Codable
struct User {
@CodingKey("user_name") // 解码使用 "user_name", "name"
@EncodingKey("name") // 编码使用 "name"
var name: String
}
解码失败时可以使用默认值, 原生 Codable
针对非 Optional
属性, 会在没有解析到正确值是抛出异常, 即使已经设置了初始值, 或者即使是 Optional
类型的枚举
@Codable
struct User {
var age: Int = 33
var name: String = "phoenix"
// 若 JSON 中不包含 gender, 原生 Codable 会抛出异常, ReerCodable 不会, 会设置其为 nil
var gender: Gender?
}
enum Gender {
case male, female
}
使用 @CodingIgnored
在编解码过程中忽略特定属性. 在解码过程中对于非 Optional
属性要有一个默认值才能满足 Swift 初始化的要求, ReerCodable
对基本数据类型和集合类型会自动生成默认值, 如果是其他自定义类型, 则需用用户提供默认值.
@Codable
struct User {
var name: String
@CodingIgnored
var ignore: Set<String>
}
自动处理 base64 字符串与 Data
, [UInt8]
类型的转换:
@Codable
struct User {
@Base64Coding
var avatar: Data
@Base64Coding
var voice: [UInt8]
}
使用 @CompactDecoding
在解码数组时自动过滤 null 值, 与 compactMap
是同样的意思:
@Codable
struct User {
@CompactDecoding
var tags: [String] // ["a", null, "b"] 将被解码为 ["a", "b"]
}
支持多种日期格式的编解码:
ReerCodable | JSON |
---|---|
@Codable
class DateModel {
@DateCoding(.timeIntervalSince2001)
var date1: Date
@DateCoding(.timeIntervalSince1970)
var date2: Date
@DateCoding(.secondsSince1970)
var date3: Date
@DateCoding(.millisecondsSince1970)
var date4: Date
@DateCoding(.iso8601)
var date5: Date
@DateCoding(.formatted(Self.formatter))
var date6: Date
static let formatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
return dateFormatter
}()
} |
{
"date1": 1431585275,
"date2": 1731585275.944,
"date3": 1731585275,
"date4": 1731585275944,
"date5": "2024-12-10T00:00:00Z",
"date6": "2024-12-10T00:00:00.000"
} |
通过 @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) throws -> UInt {
var temp: String = try decoder.value(forKeys: "rank")
temp.removeLast(2)
return UInt(temp) ?? 0
}
static func encode(by encoder: any Encoder, _ value: UInt) throws {
try encoder.set(value, forKey: "rank")
}
}
@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)
}
}
使用 @InheritedCodable
更好地支持子类的编解码. 原生 Codable
无法解析子类属性, 即使 JSON 中存在该值, 需要手动实现 init(from decoder: Decoder) throws
@Codable
class Animal {
var name: String
}
@InheritedCodable
class Cat: Animal {
var color: String
}
为枚举提供丰富的编解码能力:
- 对基本枚举类型, 以及 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?) }
- 第二种是枚举值和其关联值同级或自定义匹配的结构, 使用
.pathValue()
进行自定义路径值的匹配
@Codable enum Video1: Codable { /// { /// "type": { /// "middle": "youtube" /// } /// } @CodingCase(match: .pathValue("type.middle.youtube")) case youTube /// { /// "type": "vimeo", /// "ID": "234961067", /// "minutes": 999999 /// } @CodingCase( match: .pathValue("type.vimeo"), 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: .pathValue("type.tiktok"), values: [.label("url", keys: "media")] ) case tiktok(url: URL, tag: String?) }
- 第一种是也是原生
支持编解码的生命周期回调:
@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 {
// 在编码前进行处理
}
}
提供便捷的 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)
支持基本数据类型之间的自动转换:
@Codable
struct User {
@CodingKey("is_vip")
var isVIP: Bool // "1" 或 1 都可以被解码为 true
@CodingKey("score")
var score: Double // "100" 或 100 都可以被解码为 100.0
}
通过 AnyCodable
实现对 Any
类型的编解码:
@Codable
struct Response {
var data: AnyCodable // 可以存储任意类型的数据
var metadata: [String: AnyCodable] // 相当于[String: Any]类型
}
以上示例展示了 ReerCodable 的主要特性,这些特性可以帮助开发者大大简化编解码过程,提高代码的可读性和可维护性。