瓢虫使您能够在 Swift 4 中轻松编写模型或数据模型层。无需头疼即可完整符合 Codable
。
Codable
与 JSONCodable
? 瓢虫提供了 JSONCodable
协议,该协议是 Codable
协议的一个子协议。让我们比较一下使用 Codable
和使用 JSONCodable
创建对象的方式。
让我们用 Tree
来模拟一个模型。我想让这个对象符合 Codable
,以便可以将其从 JSON 解析出来并将其编码为 JSON。
这里有一些 JSON
{
"tree_names": {
"colloquial": ["pine", "big green"],
"scientific": ["piniferous scientificus"]
},
"age": 121,
"family": 1,
"planted_at": "7-4-1896",
"leaves": [
{
"size": "large",
"is_attached": true
},
{
"size": "small",
"is_attached": false
}
]
}
Codable
Tree: Codable
实现struct Tree: Codable {
enum Family: Int, Codable {
case deciduous, coniferous
}
let name: String
let family: Family
let age: Int
let plantedAt: Date
let leaves: [Leaf]
enum CodingKeys: String, CodingKey {
case names = "tree_names"
case family
case age
case plantedAt = "planted_at"
case leaves
}
enum NameKeys: String, CodingKey {
case name = "colloquial"
}
enum DecodingError: Error {
case emptyColloquialNames
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let namesContainer = try values.nestedContainer(keyedBy: NameKeys.self, forKey: .names)
let names = try namesContainer.decode([String].self, forKey: .name)
guard let firstColloquialName = names.first else {
throw DecodingError.emptyColloquialNames
}
name = firstColloquialName
family = try values.decode(Family.self, forKey: .family)
age = try values.decode(Int.self, forKey: .age)
plantedAt = try values.decode(Date.self, forKey: .plantedAt)
leaves = try values.decode([Leaf].self, forKey: .leaves)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var nameContainer = container.nestedContainer(keyedBy: NameKeys.self, forKey: .names)
let colloquialNames = [name]
try nameContainer.encode(colloquialNames, forKey: .name)
try container.encode(family, forKey: .family)
try container.encode(age, forKey: .age)
try container.encode(plantedAt, forKey: .plantedAt)
try container.encode(leaves, forKey: .leaves)
}
struct Leaf: Codable {
enum Size: String, Codable {
case small, medium, large
}
let size: Size
let isAttached: Bool
enum CodingKeys: String, CodingKey {
case isAttached = "is_attached"
case size
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
size = try values.decode(Size.self, forKey: .size)
isAttached = try values.decode(Bool.self, forKey: .isAttached)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(size, forKey: .size)
try container.encode(isAttached, forKey: .isAttached)
}
}
}
Codable
是 Swift 的一大步,但要如你所见,它可以非常迅速地变得复杂。
Tree: Codable
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let tree = try decoder.decode(Tree_Codable.self, from: jsonData)
Tree: Codable
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy"
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(dateFormatter)
let data = try encoder.encode(tree)
通过符合 JSONCodable
协议,您可以在仍然获得 Codable
符合性的同时跳过所有与 Codable
一起带来的样板代码。
Tree: JSONCodable
实现struct Tree: JSONCodable {
enum Family: Int, Codable {
case deciduous, coniferous
}
let name: String
let family: Family
let age: Int
let plantedAt: Date
let leaves: [Leaf]
static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
"name": JSONKeyPath("tree_names", "colloquial", 0),
"plantedAt": "planted_at" <- format("MM-dd-yyyy"),
"leaves": [Leaf].transformer,
]
struct Leaf: JSONCodable {
enum Size: String, Codable {
case small, medium, large
}
let size: Size
let isAttached: Bool
static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
"isAttached": "is_attached"
]
}
}
如你所见,您只需要提供不显式映射到属性名称的 JSON 键的映射。
Tree: JSONCodable
let tree = try Tree(data: jsonData)
Tree: JSONCodable
let data = try tree.toData()
Ladybug可以让你在Swift中创建模型时节省时间和精力,因为它提供了无需烦恼的Codable
遵循。
你可以将从JSON对象或Data
中解码任何遵循JSONCodable
的对象或对象数组。
/// Decode the given object from a JSON object
init(json: Any) throws
/// Decode the given object from `Data`
init(data: Data) throws
示例
let tree = try Tree(json: treeJSON)
let forest = try Array<Tree>(json: [treeJSON, treeJSON, treeJSON])
如果解码失败,两个初始化器都将抛出异常。
你可以将任何遵循JSONCodable
的对象或对象数组编码为JSON对象或Data
。
/// Encode the object into a JSON object
func toJSON() throws -> Any
/// Encode the object into Data
func toData() throws -> Data
示例
let jsonObject = try tree.toJSON()
let jsonData = try forest.toData()
如果编码失败,这两个函数都将抛出异常。
通过遵循Ladybug提供的JSONCodable
协议,你可以使用Data
或JSON对象来初始化任何struct
或final class
。如果你的JSON结构与数据模型不一致,你可以覆盖静态transformersByPropertyKey
属性来提供自定义映射。
struct Flight: JSONCodable {
enum Airline: String, Codable {
case delta, united, jetBlue, spirit, other
}
let airline: Airline
let number: Int
static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
"number": JSONKeyPath("flight_number")
]
}
...
let flightJSON = [
"airline": "united",
"flight_number": 472,
]
...
let directFlight = try Flight(json: flightJSON)
let flightWithLayover = try Array<Flight>(json: [flightJSON, otherFlightJSON])
directFlight
和flightWithLayover
状态完全初始化,可以编码和解码。就这么简单。
**注意**:任何嵌套枚举都必须遵循Codable
和RawRepresentable
,其中RawValue
必须是Codable
。
**注意**:PropertyKey
是String
的自定义别名。
您可以通过遵循JSONTransformer
的不同对象将JSON键与属性相关联。
变换器通过JSONCodable
协议的只读static
属性提供,并按PropertyKey
索引。
static var transformersByPropertyKey: [PropertyKey: JSONTransformer] { get }
好吧,事情会变得有点复杂,但请相信我,这很容易。
JSONKeyPath
在开头示例中,我们使用JSONKeyPath
将tree_name
字段关联的值映射到Tree
的name
属性。
JSONKeyPath
用于访问JSON中的值。它可以初始化为一系列JSON子脚本(Int
或String
)的可变数量列表。
在下面的示例中
JSONKeyPath("foo")
映射到{"hello": "world"}
"foo"
映射到{"hello": "world"}
JSONKeyPath("foo", "hello")
映射到"world"
JSONKeyPath("bar", 0)
映射到"lorem"
{
"foo": {
"hello": "world"
},
"bar": [
"lorem",
"ipsum"
]
}
**注意**:这些键路径在符合JSONTransformer
的对象中作为可选使用,当要映射的属性名与JSON结构不匹配时。如果属性名与键路径相同,则不需要包含键路径。
**注意**:JSONKeyPath也可以表示为一个字符串字面量。
JSONKeyPath("some_key")
=="some_key"
**注意**:你也可以使用Objective-C的键路径表示法。
JSONKeyPath("foo", "hello")
==JSONKeyPath("foo.hello")
=="foo.hello"
这对于Int
索引不适用
JSONKeyPath("bar", 1)
!=JSONKeyPath("bar.1")
让我们添加一个嵌套类,Passenger
。航班有乘客。很好。
您可以通过遵循JSONCodable
的任何对象或对象数组的静态transformer
属性来表示嵌套对象。
可以使用 <-
运算符来组合变压器。在这种情况下,对于 airMarshal
属性,既要指定键路径,也要指定嵌套对象需要进行显式的转换。
{
"airline": "united",
"flight_number": 472,
"air_marshal" {
"name": "50 Cent",
},
"passengers": [
{
"name": "Jennifer Lawrence",
},
{
"name": "Chris Pratt"
},
...
]
}
struct Flight: JSONCodable {
...
let passengers: [Passenger]
let airMarshal: Passenger
...
static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
...
"passengers": [Passenger].transformer,
"airMarshal": "air_marshal" <- Passenger.transformer
]
struct Passenger: JSONCodable {
let name: String
}
}
注意:在使用 <-
运算符时,始终先将 JSONKeyPath
变换器放在第一位。
最后,让我们把日期添加进去。Ladybug 提供了多个日期转换器
secondsSince1970
:将日期解码为自 1970 年以来的 JSON 数字形式的 UNIX 时间戳。millisecondsSince1970
:将日期解码为自 1970 年以来的 UNIX 毫秒时间戳形式的 JSON 数字。iso8601
:将日期解码为 ISO-8601 格式的字符串(采用 RFC 3339 格式)。format(_ format: String)
:使用自定义日期格式字符串解码日期。custom(_ adapter: @escaping (Any?) -> Date?)
:从 JSON 值返回一个 Date
。{
"july4th": "7/4/1776",
"y2k": 946684800,
}
struct SomeDates: JSONCodable {
let july4th: Date
let Y2K: Date
let createdAt: Date
static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
"july4th": format("MM/dd/yyyy"),
"Y2K": "y2k" <- secondsSince1970,
"createdAt": custom { _ in return Date() }
]
}
注意:如果使用 custom
将其映射到非可选的 Date
,则返回 nil
将导致在解码过程中抛出异常。
Map<T: Codable>
如果您需要从 JSON 值到属性的简单映射,请使用 MapTransformer
。使用该转换器将字符串转换为整数是一个很好的例子。
{
"count": "100"
}
...
struct BottlesOfBeerOnTheWall: JSONCodable {
let count: Int
static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
"count": Map<Int> { return Int($0 as! String) }
]
}
如果您想为一个属性提供默认值,您可以使用 Default
。您提供默认值,还可以控制是否覆盖 JSON 有效负载中存在的属性键。
init(value: Any, override: Bool = false)
当 API 发生变化时,默认转换器非常有用,它可以帮助将缓存的 JSON 数据迁移到具有新属性的 JSONCodable
对象。
JSONCodable
作为泛型约束因为 Array
并未显式遵循 JSONCodable
,所以 JSONCodable
在用作泛型约束时,不支持列表类型。如果您需要这项支持,可以使用 List<T: JSONCodable>
包装类型。
struct Tweet: JSONCodable { ... }
class ContentProvider<T: JSONCodable> { ... }
let tweetDetailProvider = ContentProvider<Tweet>()
let timelineProvider = ContentProvider<List<Tweet>>()
Ladybug 是以错误驱动的,所有 JSONCodable
初始化器和编码机制在失败时都会抛出异常。如果发生类型转换错误,Ladybug 会抛出 JSONCodableError
类型,Ladybug 还会从 JSONSerialization
、JSONDecoder
和 JSONEncoder
抛出异常。
如果 JSON 负载中的某个值是可选的,则在您的数据模型中也应该是可选的。Ladybug 只会在键丢失且属性将其映射到非可选属性时抛出异常。孩子们,为了保险起见,使用可选值。
有两个转换器可以返回 nil
值:`Map
如果您正在从已经编码的 `JSONCodable` 对象中解码,返回 `nil` 是完全可以的。
如果您正在从 `URLResponse` 解码,返回 `nil` 可能会导致抛出异常。
JSONCodable
当您使一个 类
符合 JSONCodable
时,有 2 个小的注意事项需要记住
您可以在 ClassConformanceTests.swift 中看到示例。
Codable
的对象派生子类将不起作用,因此它也不适用于 JSONCodable
。由于这些注意事项,我建议您使用结构体来设计数据模型。
AnyKeyPath
如果 Swift 4 中的键路径公开了一个字符串值,我们就可以使用 PartialKeyPath<Self>
作为我们的 PropertyKey
typealias,而不是使用 String
。这将是一个更安全的替代方案。
typealias PropertyKey = PartialKeyPath<Self>
...
static var transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
\Tree.name: JSONKeyPath("tree_name")
]
关于这一点,在 SE-0161 中没有讨论。
Mirror
可以为类型而不是实例创建,那就更棒了这允许我们隐式映射符合 JSONCodable
的嵌套对象。
Codable
的问题是什么? 如前所述,Codable
是简化 Swift 中 JSON 解析的巨大进步,但使用 Codable
时仍然存在 O(n) 的样板代码,这是 Swift JSON 解析中的主要问题(例如,对于对象拥有的每个属性,您需要写 1 或多行代码来将 JSON 映射到相应的属性)。在 Apple 的关于 编码和自定义类型解码 的文档中,您可以看到,一旦 JSON 键与属性键不同,就需要编写大量样板代码来获取 Codable
的符合性。Ladybug 通常会跳过这个问题,并在底层为您完成大量工作。
MapTransformer
MapTransformer
很容易过度使用。在下面的例子中,map transformer 被用来计算总和,而不是映射 JSON 值到 Codable
类型。对我来说,这促进了不好的数据建模。我坚信,数据模型应该与 JSON 响应紧密对应。如果使用不当,映射转换器可能会给数据模型太多的责任。
{
"values": [1, 1, 2, 3, 5, 8, 13]
}
...
struct FibonacciSequence: JSONCodable {
let values: [Int]
let sum: Int
static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
"sum": MapTransformer<Int>(keyPath: "values") { value in
let values = value as! [Int]
return values.reduce(0) { $0 + $1 }
}
]
}
向 Mantle 中的好人表示感谢,他们给了我一些关于这个项目的灵感。我很高兴这个类似的框架最终可以用于 Swift,而不需要混合 Obj-C 运行时。
请随时通过电子邮件 [email protected] 或在推特上联系我 戳我。我很乐意听听您的想法,或者看到这个是如何被使用的示例。