BackedCodable

强大的属性包装器,用于支持可编码的属性。

为什么?

Swift 的 Codable 是一个很棒的语言特性,但当您的序列化文件(JSON、Plist)与您实际为应用程序建模时所需的模型不同时,它很容易变得冗长并需要大量的样板代码。

BackedCodable 提供了一个单一的属性包装器,以声明式的方式注释您的属性,而不是传统的命令式 init(from decoder: Decoder)

其他 也使用属性包装器来解决 Decodable 问题,但在我看来,它们的局限性在于您每个属性只能应用一个属性包装器。因此,例如,您必须在 @LossyArray@DefaultEmptyArray 之间进行选择。

使用此库,您将能够编写诸如 @Backed(Path("attributes", "dates"), options: .lossy, strategy: .secondsSince1970) 之类的代码,以解码一个有损的日期数组,该数组使用自 1970 年以来的秒数策略在嵌套字典 attributes 的键 dates 处进行解码。

安装

BackedDecodable 可以使用 Swift Package ManagerCocoaPods 安装。

用法

特性

单个 @Backed 属性包装器为您提供以下所有特性。

自定义解码路径

@Backed() // key is inferred from property name: "firstName"
var firstName: String 

@Backed("first_name") // custom key 
var firstName: String

@Backed(Path("attributes", "first_name")) // key "first_name" nested in "attributes" dictionary 
var firstName: String

@Backed(Path("attributes", "first_name") ?? "first_name")  // will try "attributes.first_name" and if it fails "first_name" 
var firstName: String

一个 Path 由不同的 PathComponent 组成

有损集合会过滤掉无效或空值项,并仅保留成功的部分。它是一种 .compactMap()

@Backed(options: .lossy) 
var items: [Item]

@Backed(options: .lossy) 
var tags: Set<String>

当键丢失、值为 null 或值格式不正确时,使用默认值

@Backed(defaultValue: .unknown) 
var itemType: ItemType

`@Backed() // defaultValue is automatically set to `nil` so decoding an optional never "fails" 
var name: String? 

每个属性的自定义日期解码策略

@Backed("start_date", strategy: .secondsFrom1970) 
var startDate: Date

@Backed("end_date", strategy: .millisecondsFrom1970) 
var endDate: Date

当单个解码策略不足时,使用自定义解码器

@Backed("foreground_color", decoder: .HSBAColor) 
var foregroundColor: UIColor

@Backed("background_color", decoder: .RGBAColor) 
var backgroundColor: UIColor

Decoder 上的扩展,以利用上述某些特性

init(from decoder: Decoder) throws {
    self.id = try decoder.decoder(String.self, at: "uuid")`
    self.title = try decoder.decoder(String.self, at: Path("attributes", "title"))`
    self.tags = try decoder.decoder([String].self, at: Path("attributes", "tags"), options: .lossy)`
}

示例

给定以下 JSON

{
    "name": "Steve",
    "dates": [1613984296, "N/A", 1613984996],
    "values": [12, "34", 56, "78"],
    "attributes": {
        "values": ["12", 34, "56", 78],
        "all dates": {
            "start_date": 1613984296000,
            "end_date": 1613984996
        }
    },
    "counts": {
        "apples": 12,
        "oranges": 9,
        "bananas": 6
    },
    "foreground_color": {
        "hue": 255,
        "saturation": 128,
        "brightness": 128
    },
    "background_color": {
        "red": 255,
        "green": 128,
        "blue": 128
    },
    "birthdays": {
        "Steve Jobs": -468691200,
        "Tim Cook": -289238400
    }
}

所有这些都是可能的

public struct BackedStub: BackedDecodable, Equatable {
    public init(_:DeferredDecoder) {}

    @Backed()
    public var someString: String?

    @Backed()
    public var someArray: [String]?

    @Backed()
    public var someDate: Date?

    @Backed(strategy: .secondsSince1970)
    public var someDateSince1970: Date?

    @Backed("full_name" ?? "name" ?? "first_name")
    public var name: String

    @Backed(Path("attributes", "all dates", "start_date"), strategy: .deferredToDecoder)
    public var startDate: Date

    @Backed(Path("attributes", "all dates", "end_date"), strategy: .secondsSince1970)
    public var endDate: Date

    @Backed("dates", options: .lossy, strategy: .secondsSince1970)
    public var dates: [Date]

    @Backed("values", defaultValue: [], options: .lossy)
    public var values: [String]

    @Backed(Path("attributes", "values"), options: .lossy)
    public var nestedValues: [String]?

    @Backed(Path("attributes", "values", 1))
    public var nestedInteger: Int

    @Backed(Path("counts", .allKeys), options: .lossy)
    public var fruits: [Fruits]

    @Backed(Path("counts", .allValues))
    public var counts: [Int]

    @Backed(Path("counts", .allKeys, 0))
    public var bestFruit: String

    @Backed(Path("counts", .allValues, 2))
    public var lastCount: Int

    @Backed(Path("counts", .keys(where: hasSmallCount)))
    public var smallCountFruits: [String]

    @Backed(Path("counts", .keys(where: hasSmallCount), 0))
    public var firstSmallCountFruit: String

    @Backed("foreground_color", decoder: .HSBAColor)
    public var foregroundColor: Color

    @Backed("background_color", decoder: .RGBAColor)
    public var backgroundColor: Color

    @Backed(Path("birthdays", .allValues), strategy: .secondsSince1970)
    public var birthdays: [Date]

    @Backed(Path("birthdays", .allValues, 1), strategy: .secondsSince1970)
    public var timCookBirthday: Date
}

常见问题

我该如何声明一个成员式初始化器?
struct User: BackedDecodable {
    init(_: DeferredDecoder) {} // required by BackedDecodable
    
    init(id: String, firstName: String, lastName: String) {
        self.$id = id
        self.$firstName = firstName
        self.$lastName = lastName
    }
    
    @Backed("uuid")
    var id: String

    @Backed(Path("attributes", "first_name"))
    var firstName: String
    
    @Backed(Path("attributes", "last_name"))
    var lastName: String
}
如果我忘记在自定义 .init(...) 中设置 $property 会发生什么?不幸的是,除非该属性是 Optional,否则它将崩溃。为了避免崩溃,必须确保在所有自定义 .init(...) 中设置所有 self.$property,就像上面的成员式 .init(...) 示例一样。这是一个已知的限制,我没有任何解决方案。
我是否需要让我的所有模型都由 BackedDecodable 支持?不!Backed 模型可以独立工作,并且可以由普通的 Decodable 属性组成。
性能如何?我还没有运行任何性能测试(它在待办事项列表中😉),但由于该库使用反射并为每个属性从根 Decoder 遍历嵌套容器,您可能会注意到一些性能问题。如果您这样做,请随时打开一个附有详细信息的 issue!🙏
如果所有这些都是 Swift 的一部分会不会更好?会的!我必须接受一些性能和编译时安全的权衡来制作这个库(见上文),如果这在纯 Swift 中是可能的,则可能不需要这些权衡。但幸运的是,Swift 是一种令人难以置信的社区驱动语言,核心团队发起了围绕该主题的讨论。查看一下:https://forums.swift.org/t/serialization-in-swift/46641

待办事项

感谢

作者

Jérôme Alves

许可证

BackedCodable 在 MIT 许可证下可用。有关更多信息,请参见 LICENSE 文件。