引言
在项目中网络数据解析是最普通的功能了。最常见的就是JSON数据。在项目最开始我们使用了Argo
进行模型解析。Argo+Curry+Runes完成网络数据转Model。但随着苹果发布swift 4.0 引入了 JSONDecoder、JSONEncoder,加上Argo 也不再维护。所以我们准备重新设计数据模型解析。
这里我就不介绍JSONDecoder
、Codable
相关内容了。不了解的可以先自行百度。这次只分享在实现过程中遇到的坑及解决方法。
网络请求是一个不稳定的过程,接口数据不可能和理想数据完全一样。你可能会遇到某一个字段找不到;本来是Int,结果是String(这点在php 后台非常明显);本来是String,结果来了个Int 。Swift 是一个类型严格的语言。只要类型不一样,就随时可能crash。所以要开发模型解析库,第一个要解决的问题就是类型安全问题。参考其他网络模型解析(出名的有OC 的YYModel
、 MJExtension
,swift有之前使用的Argo
)。在数据解析过程中如果接口数据不是目标类型,那么会做一次类型转换。那么在Swift 中是否也可以这样做那。先来看下Swift 的类型转换方法吧!
键值对的解析 KeyedDecodingContainer
public func decode(_ type: Bool.Type, forKey key: KeyedDecodingContainer.Key) throws -> Bool
public func decode(_ type: String.Type, forKey key: KeyedDecodingContainer.Key) throws -> String
public func decode(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T where T : Decodable
这里可以看到解析时要先传入类型和相对应的key。我尝试过。这种解析只会给什么类型就解析什么。不能达到我们前面提到的目的。所以只有自己动手才能丰衣足食啊。
首先来分析下原生解析。对于绝大多数情况,一般我们定义什么类型就期望解析成什么类型。比如
struct Person: Decodable {
let name: String
let age: Int
enum PersonCodingKey: String, CodingKey {
case name
case age
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PersonCodingKey.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
}
}
但这里String.self 参数其实可以去掉。因为Swift 是支持泛型的。理想情况是我告诉container 我要解析.name
。那么它应该能够推断出String 类型。
那么我们最终的处理结果是
- 要对数据类型容错
- 避免重复类型的声明,让编译器自动推导
最终我封装成了下面的代码
// MARK: - 键值对解码
extension KeyedDecodingContainer {
func decode(key: KeyedDecodingContainer.Key) throws -> T {
var value: T
if T.self == Int.self {
value = try _safeDecode(Int.self, forKey: key) as! T
}else if T.self == Int8.self {
value = try _safeDecode(Int8.self, forKey: key) as! T
}else if T.self == Int16.self {
value = try _safeDecode(Int16.self, forKey: key) as! T
}else if T.self == Int32.self {
value = try _safeDecode(Int32.self, forKey: key) as! T
}else if T.self == Int64.self {
value = try _safeDecode(Int64.self, forKey: key) as! T
}else if T.self == UInt8.self {
value = try _safeDecode(UInt8.self, forKey: key) as! T
}else if T.self == UInt16.self {
value = try _safeDecode(UInt16.self, forKey: key) as! T
}else if T.self == UInt32.self {
value = try _safeDecode(UInt32.self, forKey: key) as! T
}else if T.self == UInt64.self {
value = try _safeDecode(UInt64.self, forKey: key) as! T
}else if T.self == String.self {
value = _safeDecode(String.self, forKey: key) as! T
}else if T.self == Bool.self {
value = try _safeDecode(Bool.self, forKey: key) as! T
}else if T.self == Float.self {
value = try _safeDecode(Float.self, forKey: key) as! T
}else if T.self == Double.self {
value = try _safeDecode(Double.self, forKey: key) as! T
}else {
value = try decode(T.self, forKey: key)
}
return value
}
private func _safeDecode(_ type: Int.Type, forKey key: KeyedDecodingContainer.Key) throws -> Int {
var value: Int?
do {
value = try decode( Int.self, forKey: key)
} catch {
let err = error as! DecodingError
switch err {
case .typeMismatch(_, _):
value = try (decodeIfPresent(String.self, forKey: key) as NSString?)?.integerValue
if value == nil {
throw err
}
default:
throw err
}
}
return value!
}
}
篇幅有限不全贴代码了。思路就是对所有基本类型做封装,如果直接转换失败,遇到了typeMismatch
错误。那么再次尝试其他类型转换。比如常见的String -> Int
String->Float, String -> Double, Int > Bool ,String -> Bool等。
所以现在的模型解析就可以简化成这样了。
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PersonCodingKey.self)
name = try container.decode(key: .name)
age = try container.decode(key: .age)
}
这样解析语义也更加清晰。
按照这个思路对其他进行扩展
extension SingleValueDecodingContainer {
func decode() throws -> T {
var value: T
if T.self == Int.self {
value = try _safedecode(Int.self) as! T
}else if T.self == String.self {
value = try _safedecode(String.self) as! T
} else if T.self == Float.self {
value = try _safedecode(Float.self) as! T
}else {
value = try decode(T.self)
}
return value
}
}
数组解析
extension KeyedDecodingContainer {
//MARK: collection解码
/* 序列编码 目前只自定义了Array */
func sequenceDecode(key: KeyedDecodingContainer.Key) throws -> T where T.Element: Decodable {
var values: T
if T.self == Array.self {
var array = Array.init()
var unkeyed = try nestedUnkeyedContainer(forKey: key)
while !unkeyed.isAtEnd {
var subValue: T.Element? = nil
if T.Element.self == Int.self {
subValue = decode(Int.self, unkeyed: &unkeyed) as? T.Element
}else if T.Element.self == String.self {
subValue = decode(String.self, unkeyed: &unkeyed) as? T.Element
}else if T.Element.self == Float.self {
subValue = decode(Float.self, unkeyed: &unkeyed) as? T.Element
}else {
subValue = try unkeyed.decode(T.Element.self)
}
if subValue != nil {
array.append(subValue!)
}
}
values = array as! T
}else{
values = try decode(T.self, forKey: key)
}
return values
}
}
我只对基本类型进行了扩展。防止某一个item解析失败导致整个数组解析都失败。
在实际开发中有些字段并不是must 的,所以一些optional 字段是可以解析失败的。于是又扩展了对 optional 字段解析
/* 解码optional 的 不会转码失败 */
func decodeIfPresent(key: KeyedDecodingContainer.Key) -> T? {
var value: T??
if T.self == Int.self {
value = try? _safeDecode(Int.self, forKey: key) as? T
}else if T.self == Int8.self {
value = try? _safeDecode(Int8.self, forKey: key) as? T
}else if T.self == Int16.self {
value = try? _safeDecode(Int16.self, forKey: key) as? T
}else if T.self == Int32.self {
value = try? _safeDecode(Int32.self, forKey: key) as? T
}else if T.self == Int64.self {
value = try? _safeDecode(Int64.self, forKey: key) as? T
}else if T.self == UInt8.self {
value = try? _safeDecode(UInt8.self, forKey: key) as? T
}else if T.self == UInt16.self {
value = try? _safeDecode(UInt16.self, forKey: key) as? T
}else if T.self == UInt32.self {
value = try? _safeDecode(UInt32.self, forKey: key) as? T
}else if T.self == UInt64.self {
value = try? _safeDecode(UInt64.self, forKey: key) as? T
}else if T.self == String.self {
value = _safeDecode(String.self, forKey: key) as? T
}else if T.self == Bool.self {
value = try? _safeDecode(Bool.self, forKey: key) as? T
}else if T.self == Float.self {
value = try? _safeDecode(Float.self, forKey: key) as? T
}else if T.self == Double.self {
value = try? _safeDecode(Double.self, forKey: key) as? T
}else {
value = try? decode(T.self, forKey: key) as T?
}
if value == nil {
return nil
}
return value!!
}
这样既可以保证数据的正确性又有很强的容错性。
struct LifeStyleItem: JSONDecodable {
let name: String //生活指数名字
let icon: String
let desc: String? //生活指数描述
let suggest: String? //建议,目前服务器暂时不传
let color: String //取值范围:"red", "green",red表示超标,green在安全值范围内
let type: LifeStyleType // "1"表示原生指数,"2"表示运营指数,等等(穿衣指数属于原生指数),目前服务器暂时不传,客户端保留,默认值为1
}
extension LifeStyleItem {
private enum LifeStyleItemCodingKey: String, CodingKey {
case name
case icon
case desc
case suggest
case color
case type
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: LifeStyleItemCodingKey.self)
name = try container.decode(key: .name)
icon = try container.decode(key: .icon)
desc = container.decodeIfPresent(key: .desc)
suggest = container.decodeIfPresent(key: .suggest)
color = try container.decode(key: .color)
type = try container.decode(key: .type)
}
}
整个解析差不多已经OK。现在还差最后一点。与网络接口对接。
于是定义了以下协议:
protocol JSONDecodable: Decodable {
static func decode(_ json: [String: Any]?) -> Self?
static func decode(_ json: [[String: Any]?]?) -> [Self]
}
extension JSONDecodable {
static func decode(_ json: [String: Any]?) -> Self? {
guard json != nil else {
return nil
}
let data = try? JSONSerialization.data(withJSONObject: json!, options: JSONSerialization.WritingOptions(rawValue: 0))
guard data != nil else {
return nil
}
let decode = JSONDecoder.init()
var model: Self? = nil
do {
model = try decode.decode(Self.self, from: data!)
} catch {
print("decode \(Self.self) error \(error)")
}
return model
}
static func decode(_ json: [[String: Any]?]?) -> [Self] {
guard json != nil else {
return []
}
var models: [Self] = []
let decode = JSONDecoder.init()
json!.forEach({ (jsonData: [String: Any]?) in
if jsonData == nil {
return
}
let data = try? JSONSerialization.data(withJSONObject: jsonData!, options: JSONSerialization.WritingOptions(rawValue: 0))
if data == nil {
return
}
var model: Self?
do{
model = try decode.decode(Self.self, from: data!)
}catch{
return
}
models.append(model!)
})
return models
}
}
protocol JSONEncodable: Encodable {
func encode() -> [String: Any]?
}
extension JSONEncodable {
func encode() -> [String: Any]? {
let encoder = JSONEncoder()
let codedValue = try? encoder.encode(self)
guard let codeData = codedValue else {
return nil
}
let json = try? JSONSerialization.jsonObject(with: codeData, options: JSONSerialization.ReadingOptions(rawValue: 0))
return json as? [String: Any]
}
}
typealias JSONCodable = JSONDecodable & JSONEncodable
这样模型就遵循 JSONCodable
并实现 Decodable
、Encodable
协议就可以完成模型解析。
最后解析可以简化为
let festival = Festival.decode(json)