这是Swift数据解析计划的系列文章:

Swift数据解析(第一篇) – 技能选型

Swift数据解析(第二篇) – Codable 上

Swift数据解析(第二篇) – Codable 下

Swift数据解析(第三篇) – Codable源码学习

Swift数据解析(第四篇) – SmartCodable 上

Swift数据解析(第四篇) – SmartCodable 下

运用Codable 协议 进行 decode 时分,遇到以下三种状况就会失利。并且只要一个特点解析失利时就抛出反常,导致整个解析失利:

  • 类型键不存在
  • 类型键不匹配
  • 数据值是null

SmartCodable 旨在兼容处理 Codable 解码抛出的反常,使解析顺利进行下去。

SmartCodable 供给穷尽了各种反常场景验证兼容性,均成功兼容。

SmartCodable的github地址

Swift数据解析(第四篇) - SmartCodable(下)

环境要求

Swift 5.0+

装置

Add the following line to yourPodfile:

$ pod 'SmartCodable'

Then, run the following command:

$ pod install

一. 运用SmartCodable

字典类型的解码

import SmartCodable
​
struct SimpleSmartCodableModel: SmartCodable {
 var name: String = ""
}
​
let dict: [String: String] = ["name": "xiaoming"]
guard let model = SimpleSmartCodableModel.deserialize(dict: dict) else { return }
print(model.name)

数组类型的解码

import SmartCodable
​
struct SimpleSmartCodableModel: SmartCodable {
 var name: String = ""
}
​
let dict: [String: String] = ["name": "xiaoming"]
let arr = [dict, dict]
guard let models = [SimpleSmartCodableModel].deserialize(array: arr) else { return }
print(models)

序列化与反序列化

// 字典转模型
guard let xiaoMing = JsonToModel.deserialize(dict: dict) else { return }
​
// 模型转字典
let studentDict = xiaoMing.toDictionary() ?? [:]
​
// 模型转json字符串
let json1 = xiaoMing.toJSONString(prettyPrint: true) ?? ""// json字符串转模型
guard let xiaoMing2 = JsonToModel.deserialize(json: json1) else { return }

二. SmartCoable 解析增强

解析完结的回调

class FinishMappingSingle: SmartDecodable {
​
 var name: String = ""
 var age: Int = 0
 var desc: String = ""
 
 required init() { }
 
 func didFinishMapping() {
       
   if name.isEmpty {
     desc = "(age)岁的" + "人"
   } else {
     desc = "(age)岁的" + name
   }
 }
}

当结束decode之后,会经过该办法回调。供给该类在解析完结进一步对值处理的才能。

字段重命名

let dict = [
 "name": "xiaoming",
 "class_name": "35班"
] as [String : Any]
​
guard let feed1 = FieldNameMapSingle.deserialize(dict: dict) else { return }
 guard let feed2 = FieldNameMapDouble.deserialize(dict: dict) else { return }
​
​
// 1. 单个字段映射成模型字段。
struct FieldNameMapSingle: SmartCodable {
  var name: String = ""
  var className: String = ""
  /// 字段映射
  static func mapping() -> JSONDecoder.KeyDecodingStrategy? {
    .mapper([
      "class_name": "className",
    ])
  }
}
struct FieldNameMapDouble: SmartCodable {
  var name: String = ""
  var className: String = ""
  /// 字段映射
  static func mapping() -> JSONDecoder.KeyDecodingStrategy? {
    // 支撑多余值的兼容,相同值的兼容
    // 数据中有 两个 映射字段到同一个特点值,由所以解析的字典(无序),所以详细运用哪个值。
    .mapper([
      ["class_name", "other1", "other1", "className"]: "className",
      ["class_name"]: "name"
    ])
  }
}

经过完成mapping办法,返回解码key的映射联系。

三. SmartCodable的兼容性

兼容策略

smartCodable 的兼容性是从两方面设计的:

  • 类型兼容:假如值对应的真实类型和特点的类型不匹配时,测验对值进行类型转化,假如能够转化成功,就运用转化之后值填充。
  • 默认值兼容:当解析失利的时分,会供给特点类型对应的默认值进行填充。

1. 类型转化兼容策略

/// 类型兼容器,担任测验兼容类型不匹配,只兼容数据有意义的状况(能够合理的进行类型转化的)。
struct TypeCumulator<T: Decodable> {
 static func compatible(context: DecodingError.Context, originDict: [String: Any]) -> T? {
   if let lastKey = context.codingPath.last?.stringValue {
     if let value = originDict[lastKey] {
       
       switch T.self {
       case is Bool.Type:
         let smart = compatibleBoolType(value: value)
         return smart as? Tcase is String.Type:
         let smart = compatibleStringType(value: value)
         return smart as? Tcase is Int.Type:
         let smart = compatibleIntType(value: value)
         return smart as? Tcase is Float.Type:
         let smart = compatibleFloatType(value: value)
         return smart as? T
         
       case is CGFloat.Type:
         let smart = compatibleCGFloatType(value: value)
         return smart as? Tcase is Double.Type:
         let smart = compatibleDoubleType(value: value)
         return smart as? T
       default:
         break
       }
     }
   }
   return nil
 }
​
 
 /// 兼容Bool类型的值,Model中界说为Bool类型,可是数据中是String,Int的状况。
 static func compatibleBoolType(value: Any) -> Bool? {
   switch value {
   case let intValue as Int:
     if intValue == 1 {
       return true
     } else if intValue == 0 {
       return false
     } else {
        return nil
     }
   case let stringValue as String:
     switch stringValue {
     case "1", "YES", "Yes", "yes", "TRUE", "True", "true":
       return true
     case "0", "NO", "No", "no", "FALSE", "False", "false":
       return false
     default:
       return nil
     }
   default:
     return nil
   }
 }
 
 
 /// 兼容String类型的值
 static func compatibleStringType(value: Any) -> String? {
   
   switch value {
   case let intValue as Int:
     let string = String(intValue)
     return string
   case let floatValue as Float:
     let string = String(floatValue)
     return string
   case let doubleValue as Double:
     let string = String(doubleValue)
     return string
   default:
     return nil
   }
 }
 
 /// 兼容Int类型的值
 static func compatibleIntType(value: Any) -> Int? {
   if let v = value as? String, let intValue = Int(v) {
     return intValue
   }
   return nil
 }
 
 /// 兼容 Float 类型的值
 static func compatibleFloatType(value: Any) -> Float? {
   if let v = value as? String {
     return v.toFloat()
   }
   return nil
 }
 
 /// 兼容 double 类型的值
 static func compatibleDoubleType(value: Any) -> Double? {
   if let v = value as? String {
     return v.toDouble()
   }
   return nil
 }
 
 /// 兼容 CGFloat 类型的值
 static func compatibleCGFloatType(value: Any) -> CGFloat? {
   if let v = value as? String {
     return v.toCGFloat()
   }
   return nil
 }
}

2. 默认值兼容

/// 默认值兼容器
struct DefaultValuePatcher<T: Decodable> {
 
 /// 生产对应类型的默认值
 static func makeDefaultValue() throws -> T? {
​
   if let arr = [] as? T {
     return arr
     
   } else if let dict = [:] as? T {
     return dict
     
   } else if let value = "" as? T {
     return value
   } else if let value = false as? T {
     return value
   } else if let value = Date.defaultValue as? T {
     return value
   } else if let value = Data.defaultValue as? T {
     return value
   } else if let value = Decimal.defaultValue as? T {
     return value
           
   } else if let value = Double(0.0) as? T {
     return value
   } else if let value = Float(0.0) as? T {
     return value
   } else if let value = CGFloat(0.0) as? T {
     return value
     
   } else if let value = Int(0) as? T {
     return value
   } else if let value = Int8(0) as? T {
     return value
   } else if let value = Int16(0) as? T {
     return value
   } else if let value = Int32(0) as? T {
     return value
   } else if let value = Int64(0) as? T {
     return value
           
   } else if let value = UInt(0) as? T {
     return value
   } else if let value = UInt8(0) as? T {
     return value
   } else if let value = UInt16(0) as? T {
     return value
   } else if let value = UInt32(0) as? T {
     return value
   } else if let value = UInt64(0) as? T {
     return value
   } else {
     /// 判断此时的类型是否完成了SmartCodable, 假如是就阐明是自界说的结构体或类。
     if let object = T.self as? SmartDecodable.Type {
       return object.init() as? T
     } else {
       SmartLog.logDebug("(Self.self)供给默认值失利, 发现不知道类型,无法供给默热值。如有遇到请反馈,感谢")
       return nil
     }
   }
 }
}

不同场景的兼容计划

1. 键缺失的兼容

  • 非可选特点:运用默认值兼容计划。
  • 可选特点:运用nil填充。

详见demo中 CompatibleKeylessViewController 演示。

2. 值类型不匹配

  • 非可选特点:先运用类型转化兼容,兼容失利再运用默认值兼容计划。
  • 可选特点:先运用类型转化兼容,兼容失利运用nil填充。

详见demo中 CompatibleTypeMismatchViewController 演示。

3. 空目标的兼容

  • 非可选特点:运用默认值兼容计划。
  • 可选特点:运用nil填充。

详见demo中 CompatibleEmptyObjectViewController 演示。

4. null值的兼容

  • 特点为非可选,运用特点类型对应的默认值进行填充。
  • 特点为可选,运用nil填充。

详见demo中 CompatibleNullViewController 演示。

5. enum的兼容

枚举的兼容较为特殊,供给了SmartCaseDefaultable协议,假如解码失利,运用协议特点defaultCase兼容。

struct CompatibleEnum: SmartCodable {
​
 init() { }
 var enumTest: TestEnum = .a
​
 enum TestEnum: String, SmartCaseDefaultable {
   static var defaultCase: TestEnum = .a
​
   case a
   case b
   case hello = "c"
 }
}

详见demo中 CompatibleEnumViewController 演示。

6. 浮点数的兼容

  • 非可选特点:先运用类型转化兼容,兼容失利再运用默认值兼容计划。
  • 可选特点:先运用类型转化兼容,兼容失利运用nil填充。

详见demo中 CompatibleFloatViewController 演示。

7. Bool的兼容

  • 非可选特点:先运用类型转化兼容,兼容失利再运用默认值兼容计划。
  • 可选特点:先运用类型转化兼容,兼容失利运用nil填充。

详见demo中 CompatibleBoolViewController 演示。

8. String的兼容

  • 非可选特点:先运用类型转化兼容,兼容失利再运用默认值兼容计划。
  • 可选特点:先运用类型转化兼容,兼容失利运用nil填充。

详见demo中 CompatibleStringViewController 演示。

9. Int的兼容

  • 非可选特点:先运用类型转化兼容,兼容失利再运用默认值兼容计划。
  • 可选特点:先运用类型转化兼容,兼容失利运用nil填充。

详见demo中 CompatibleIntViewController 演示。

10. class的兼容

  • 非可选特点:运用默认值兼容计划。
  • 可选特点:运用nil填充。

详见demo中 CompatibleClassViewController 演示。

四. 调试日志

经过咱们的兼容,解析将不会出现问题,可是这是这掩盖了问题,并没有从根本上解决问题。假如开启了调试日志,将供给辅助信息,协助定位问题。

  • 过错类型: 过错的类型信息
  • 模型称号:发生过错的模型名出
  • 数据节点:发生过错时,数据的解码途径。
  • 特点信息:发生过错的字段名。
  • 过错原因: 过错的详细原因。
================ [SmartLog Error] ================
过错类型: '找不到键的过错'
模型称号:Array<Class>
数据节点:Index 0 → students → Index 0
特点信息:(称号)more
过错原因: No value associated with key CodingKeys(stringValue: "more", intValue: nil) ("more").
==================================================
​
================ [SmartLog Error] ================
过错类型: '值类型不匹配的过错'
模型称号:DecodeErrorPrint
数据节点:a
特点信息:(类型)Bool (称号)a
过错原因: Expected to decode Bool but found a string/data instead.
==================================================
​
​
================ [SmartLog Error] ================
过错类型: '找不到值的过错'
模型称号:DecodeErrorPrint
数据节点:c
特点信息:(类型)Bool (称号)c
过错原因: c 在json中对应的值是null
==================================================

你能够经过SmartConfig.debugMode 调整日志的打印等级。

五. SamrtCodable的缺陷

其实算是Codable的缺陷。

1. 可选模型特点

假如要解析嵌套结构,该模型特点要设置为可选,需求运用 @SmartOptional 特点包装器润饰。

struct Firend: SmartDecodable {
 var age: Int?
 var name: String = ""
 @SmartOptional var location: Location?
 var hobby: Hobby = Hobby()
}
​
class Location: SmartDecodable {
 var pronince: String = ""
 var city: String = ""
 
 required init() { }
}
​
struct Hobby: SmartDecodable {
 var name: String = ""
 init() {
   name = ""
 }
}

Firend 的 location特点 是一个恪守了 SmartDecodable 的模型。假如设置为可选特点需求运用 @SmartOptional 特点包装器润饰。

运用SmartOptional的约束

SmartOptional润饰的目标有必要满意一下三个要求:

  1. 有必要遵循SmartDecodable协议

    惯例的特点(Bool/String/Int等)没有运用 @SmartOptional 特点包装器的必要,因此做了该约束。

  2. 有必要是可选特点

    非可选的特点没有运用 @SmartOptional 特点包装器的必要,因此做了该约束。

  3. 有必要是class类型

    经过 didFinishMapping 修正该特点的值的状况下,借用 class 的引用类型的特性,达到修正的意图。能够检查 acceptChangesAfterMappingCompleted 该办法的完成。

为什么这么做?

这是一个不得已的完成计划。

  1. 为了做解码失利的兼容,咱们重写了KeyedEncodingContainer的decode和decodeIfPresent办法,这两个类型的办法均会走到兜底的smartDecode办法中。

该办法最终运用了public func decodeIfPresent(_ type: T.Type, forKey key: K) throws -> T? 完成了decode才能。

  1. KeyedEncodingContainer容器是用结构体完成的。 重写了结构体的办法之后,没办法再调用父办法。
  2. 这种状况下,假如再重写public func decodeIfPresent (*_ type: T.Type, forKey key: K) throws -> T?办法,就会导致办法的循环调用。
  3. 咱们运用SmartOptional特点包装器润饰可选的特点,被润饰后会发生一个新的类型,对此类型解码就不会走decodeIfPresent,而是会走decode办法。

2. Any无法运用

Any无法完成Codable,所以在运用Codable的时分,全部跟Any有关的均不答应,比方[String:Any],[Any]。

struct Feed: SmartCodable {
  /// Type 'Feed' does not conform to protocol 'Decodable'
  var dict: [String: Any] = [:]
}

能够经过指定类型,比方[Sting: String], 放弃Any的运用。

或者经过范型,比方

struct AboutAny<T: Codable>: SmartCodable {
 init() { }
​
 var dict1: [String: T] = [:]
 var dict2: [String: T] = [:]
}

3. 模型中设置的默认值无效

Codable在进行解码的时分,是无法知道这个特点的。所以在decode的时分,假如解析失利,运用默认值进行填充时,拿不到这个默认值。再处理解码兼容时,只能自己生成一个对应类型的默认值填充。

struct Feed: SmartCodable {
  /// 假如解码失利,会被填充 ““,导致defalut value被替换掉。
  var name: String = "defalut value"
}