简体中文

ReerCodable

使用 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 宏展开中进行展开,展开其他宏将不会有任何响应)

主要功能包括

要求

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

您可以使用 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

由于 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_xcconfigs.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 的序列化过程。 以下是每个功能的详细示例

1. 自定义 CodingKey

使用 @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
    }
}

2. 嵌套 CodingKey

支持使用点符号的嵌套键路径

@Codable
struct User {
    @CodingKey("other_info.weight")
    var weight: Double
    
    @CodingKey("location.city")
    var city: String
}

3. 用于解码的多个键

可以为解码指定多个键,系统将按顺序尝试解码,直到成功为止

@Codable
struct User {
    @CodingKey("name", "username", "nick_name")
    var name: String
}

4. 命名风格转换

支持多种命名风格转换,可以应用于类型或单个属性

@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"
}

5. 自定义 Coding Container

使用 @CodingContainer 自定义编码期间的容器路径,通常用于根级别模型解析

ReerCodable JSON
@Codable
@CodingContainer("data.info")
struct UserInfo {
    var name: String
    var age: Int
}
{
    "code": 0,
    "data": {
        "info": {
            "name": "phoenix",
            "age": 33
        }
    }
}

6. 编码特定的键

可以为编码过程指定不同的键名。 由于 @CodingKey 可能有多个参数,并且可以使用 @SnakeCaseKebabCase 等,解码可能会使用多个键,那么编码将使用第一个键,或者可以使用 @EncodingKey 指定键

@Codable
struct User {
    @CodingKey("user_name")      // decoding uses "user_name", "name"
    @EncodingKey("name")         // encoding uses "name"
    var name: String
}

7. 默认值支持

解码失败时可以使用默认值。 当未解析到正确的值时,即使已设置初始值,或者即使它是 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
}

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 在解码数组时自动过滤空值,与 compactMap 具有相同的含义

@Codable
struct User {
    @CompactDecoding
    var tags: [String]  // ["a", null, "b"] will be decoded as ["a", "b"]
}

11. 日期编码

支持各种日期格式编码/解码

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"
}

12. 自定义编码/解码逻辑

通过 @CustomCoding 实现自定义编码/解码逻辑。 有两种自定义编码/解码的方法

@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
}
// 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. 枚举支持

为枚举提供丰富的编码/解码功能

@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"
}
@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
}

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 {
        // 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)
    }
}

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. 基本类型转换

支持基本数据类型之间的自动转换

@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
}

18. AnyCodable 支持

通过 AnyCodable 实现 Any 类型的编码/解码

@Codable
struct Response {
    var data: AnyCodable  // Can store data of any type
    var metadata: [String: AnyCodable]  // Equivalent to [String: Any] type
}

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. 生成复制方法

使用 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 的主要功能,它可以帮助开发人员极大地简化编码/解码过程,从而提高代码的可读性和可维护性。