强大的属性包装器,用于支持可编码的属性。
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 Manager 或 CocoaPods 安装。
@Backed()
标记您的模型中的所有属性BackedDecodable
协议;它只需要一个 init(_:DeferredDecoder)
单个 @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
组成
.key(String)
:也可以用字符串字面量表示 (Path("foo") == Path(.key("foo"))
).index(Int)
:也可以用整数字面量表示 (Path("foo", 0) == Path(.key("foo"), .index(0))
).allKeys
:从字典中获取所有键.allValues
:从字典中获取所有值.keys(where: { key, value -> Bool })
:过滤字典的元素并提取它们的键.values(where: { key, value -> Bool })
:过滤字典的元素并提取它们的值有损集合会过滤掉无效或空值项,并仅保留成功的部分。它是一种 .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
支持?BackedCodable 在 MIT 许可证下可用。有关更多信息,请参见 LICENSE 文件。