使用 Swift 宏扩展 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() 宏,它在其他宏提供的数据注解下生成具体实现(@Codable
宏可以在 XCode 宏展开中进行展开,展开其他宏将不会有任何响应)
主要功能包括
@CodingKey("key")
为每个属性声明自定义 CodingKey
值,无需编写所有 CodingKey
值。CodingKey
,例如 @CodingKey("nested.key")
CodingKey
进行解码,例如 @CodingKey("key1", "key2")
@SnakeCase
、KebabCase
等来标记类型或属性,以便轻松进行命名转换@CodingContainer
自定义编码期间的嵌套容器CodingKey
,例如 EncodingKey("encode_key")
keyNotFound
错误@CodingIgnored
在编码/解码期间忽略特定属性@Base64Coding
在 base64 字符串和 Data
[UInt8]
类型之间自动转换@CompactDecoding
,在解码 Array
、Dictionary
、Set
时忽略 null
值,而不是抛出错误@DateCoding
支持各种 Date
的编码/解码@CustomCoding
支持自定义编码/解码逻辑@InheritedCodable
更好地支持子类enum
类型提供简单而丰富的编码/解码功能ReerCodableDelegate
支持编码/解码生命周期,例如 didDecode
、willEncode
Dictionary
、Array
作为编码/解码的参数Bool
、String
、Double
、Int
、CGFloat
)之间的转换Int128
、UInt128
AnyCodable
支持 Any
的编码/解码,例如 var dict = [String: AnyCodable]
@DefaultInstance
自动创建类型的默认实例,可通过 Model.default
访问@Copyable
宏生成强大的 copy()
方法,允许在一次调用中进行完整复制和选择性属性更新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 安装 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.7")
]
)
然后,将 ReerCodable 添加到您的目标依赖项中,如下所示
.product(name: "ReerCodable", package: "ReerCodable"),
最后,运行 swift package update
。
由于 CocoaPods 不直接支持 Swift Macro,宏实现可以编译成二进制文件以供使用。 集成方法如下,需要 s.pod_target_xcconfig
来加载宏实现的二进制插件
Pod::Spec.new do |s|
s.name = 'YourPod'
s.dependency 'ReerCodable', '1.1.7'
# Copy the following config to your 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
为属性指定自定义键,而无需手动编写 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
}
}
|
支持使用点符号的嵌套键路径
@Codable
struct User {
@CodingKey("other_info.weight")
var weight: Double
@CodingKey("location.city")
var city: String
}
可以为解码指定多个键,系统将按顺序尝试解码,直到成功为止
@Codable
struct User {
@CodingKey("name", "username", "nick_name")
var name: String
}
支持多种命名风格转换,可以应用于类型或单个属性
@Codable
@SnakeCase
struct Person {
var firstName: String // decoded from "first_name" or encoded to "first_name"
@KebabCase
var lastName: String // decoded from "last-name" or encoded to "last-name"
}
使用 @CodingContainer
自定义编码期间的容器路径,通常用于根级别模型解析
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
等,解码可能会使用多个键,那么编码将使用第一个键,或者可以使用 @EncodingKey
指定键
@Codable
struct User {
@CodingKey("user_name") // decoding uses "user_name", "name"
@EncodingKey("name") // encoding uses "name"
var name: String
}
解码失败时可以使用默认值。 当未解析到正确的值时,即使已设置初始值,或者即使它是 Optional
类型枚举,原生 Codable
也会为非 Optional
属性抛出异常
@Codable
struct User {
var age: Int = 33
var name: String = "phoenix"
// If gender is not included in JSON, native Codable will throw an exception, ReerCodable won't, it will set it to 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
在解码数组时自动过滤空值,与 compactMap
具有相同的含义
@Codable
struct User {
@CompactDecoding
var tags: [String] // ["a", null, "b"] will be decoded as ["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, 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)
}
}
使用 @InheritedCodable
以更好地支持子类编码/解码。 原生 Codable
无法解析子类属性,即使该值存在于 JSON 中,也需要手动实现 init(from decoder: Decoder) throws
@Codable
class Animal {
var name: String
}
@InheritedCodable
class Cat: Animal {
var color: String
}
为枚举提供丰富的编码/解码功能
@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: ....)
来匹配多个值或范围@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 {
// Process before encoding
}
}
@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)
}
}
提供方便的 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" or 1 can be decoded as true
@CodingKey("score")
var score: Double // "100" or 100 can be decoded as 100.0
}
通过 AnyCodable
实现 Any
类型的编码/解码
@Codable
struct Response {
var data: AnyCodable // Can store data of any type
var metadata: [String: AnyCodable] // Equivalent to [String: Any] type
}
@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
}
使用 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
方法。 正如您所看到的,除了默认复制之外,您还可以更新特定属性
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
)
}
这些示例演示了 ReerCodable 的主要功能,它可以帮助开发人员极大地简化编码/解码过程,从而提高代码的可读性和可维护性。