然后,在 Swift 代码中,想要将此类 Json 响应转换为 CurrencyConversion 实例,每个实例都包含一个 ExchangeRate 条目数组,每个币种对应一个:
struct CurrencyConversion {
var currency: Currency
var exchangeRates: [ExchangeRate]
}
struct ExchangeRate {
let currency: Currency
let rate: Double
}
但是,如果仅仅只是使以上两个模型都符合 Codable,将再次导致 Swift 代码与要解码的 Json 数据不匹配。但是这次,不只是关键字名称的问题,结构上有根本的不同。当然,可以修改 Swift 模型的结构,使其与 Json 数据的结构完全匹配,但这并不总是可行的。尽管拥有正确的序列化代码很重要,但是拥有适合实际代码库的模型结构也同样重要。
相反,创建一个新的专用类型,它将在 Json 数据中使用的格式与 Swift 代码的结构体之间架起一座桥梁。在这种类型中,我们将能够封装将 Json 汇率字典转换为一系列 ExchangeRate 模型所需的所有逻辑,如下所示:
private extension ExchangeRate {
struct List: Decodable {
let values: [ExchangeRate]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dictionary = try container.decode([String : Double].self)
values = dictionary.map { key, value in
ExchangeRate(currency: Currency(key), rate: value)
}
}
}
}
struct StringBacked<Value: StringRepresentable>: Codable {
var value: Value
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
guard let value = Value(string) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Failed to convert an instance of \(Value.self) from '\(string)'"
)
}
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value.description)
}
}
就像以前为兼容 Json 的基础存储创建私有属性的方式一样,现在可以对编码后由字符串后端的任何属性执行相同的操作,同时仍将数据适当地公开给其它 Swift 代码类型,这是一个针对视频类型的 numberOfLikes 属性执行此操作的示例:
struct Video: Codable {
var title: String
var description: String
var url: URL
var thumbnailImageURL: URL
var numberOfLikes: Int {
get { return likes.value }
set { likes.value = newValue }
}
private var likes: StringBacked<Int>
}
在必须手动为属性定义 setter 和 getter 的复杂性与必须回退到完全自定义的 Codable 实现的复杂性之间,这里肯定有一个折中,但是对于上述 Video 结构体这样的类型,它在其中仅具有一个属性需要自定义,使用私有支持属性可能是一个不错的选择。
三、Codable 将任意类型解析为想要的类型
① 常规解析
默认情况下,使用 Swift 内置的 Codable API 解析 Json 时,属性类型需要和 Json 中的类型保持一致,否则就会解析失败。
例如,现有如下 Json:
{
"name":"ydw",
"age":18
}
开发中常用的模型如下:
struct User: Codable {
var name: String
var age: Int
}
这个时候,正常解析则没有任何问题,但是:
当出现服务器将 age 中的 18 采用 String 方式 “18” 返回时,则无法解析,这是非常难遇见的情况;
另一种常见的是返回 “18.1”, 这是一个 Double 类型,这时候一样无法成功解析。
在使用 OC 的时候,常用的方法将其解析为 NSString 类型,使用的时候再进行转换,可是当使用 Swift 的 Codabel 时,就不能直接做到这样。
② 如果服务器只会以 String 方式返回 Age,同时能确认里面是 Int 还是 Double
可以使用上文中的“值转换”来完成:
protocol StringRepresentable: CustomStringConvertible {
init?(_ string: String)
}
extension Int: StringRepresentable {}
struct StringBacked<Value: StringRepresentable>: Codable {
var value: Value
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
guard let value = Value(string) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Failed to convert an instance of \(Value.self) from '\(string)'"
)
}
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value.description)
}
}
这个时候,模型如下:
{
"name":"zhy",
"age":"18"
}
struct User: Codable {
var name: String
var ageInt: Int {
get { return age.value }
set { age.value = newValue}
}
private var age: StringBacked<Int>
}
③ 不能确认是什么类型
第一种处理方法会改变原有数据结构,虽然对于直接重写 User 的解析过程来说,拥有更多的通用性,但是遇到其它情况则束手无策。第二种方法同时也不会采用重写模型自身的解析过程来实现,那样子不具备通用性,太麻烦,每次遇到都需要来一遍。
参照第一种方法,先写一个将任意类型转换成 String? 的方法:
// 不确定服务器返回什么类型,都转换为 String 然后保证正常解析
// 当前支持 Double Int String
// 其他类型会解析成 nil
//
/// 将 String Int Double 解析为 String? 的包装器
@propertyWrapper public struct YDWString: Codable {
public var wrappedValue: String?
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var string: String?
do {
string = try container.decode(String.self)
} catch {
do {
try string = String(try container.decode(Int.self))
} catch {
do {
try string = String(try container.decode(Double.self))
} catch {
// 如果不想要 String? 可以在此处给 string 赋值 = “”
string = nil
}
}
}
wrappedValue = string
}
}
此时 User 写成:
struct User: Codable {
var name: String
@YDWString public var age: String?
}
同理,可以写一个 YDWInt,来将任意类型转换为 Int,如果确实无法转换,可以控制其为 nil 或者直接等于 0,这样就可以保证不管怎么样,解析不会失败。此时 User 写成:
struct User: Codable {
var name: String
@YDWInt public var age: Int
}
看起来这个地方影响很小,只有 User 解析失败没什么,当遇到整个页面都是用一个 Json 返回时,不管是哪个局部出现问题,都会导致真个页面解析失败,所以还是要做好兼容操作最好。