这是Swift数据解析计划的系列文章:
Swift数据解析(第一篇) – 技能选型
Swift数据解析(第二篇) – Codable 上
Swift数据解析(第二篇) – Codable 下
Swift数据解析(第三篇) – Codable源码学习
Swift数据解析(第四篇) – SmartCodable 上
Swift数据解析(第四篇) – SmartCodable 下
运用Codable 协议 进行 decode 时分,遇到以下三种状况就会失利。并且只要一个特点解析失利时就抛出反常,导致整个解析失利:
- 类型键不存在
- 类型键不匹配
- 数据值是null
SmartCodable 旨在兼容处理 Codable 解码抛出的反常,使解析顺利进行下去。
SmartCodable 供给穷尽了各种反常场景验证兼容性,均成功兼容。
SmartCodable的github地址
环境要求
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? T
case is String.Type:
let smart = compatibleStringType(value: value)
return smart as? T
case is Int.Type:
let smart = compatibleIntType(value: value)
return smart as? T
case is Float.Type:
let smart = compatibleFloatType(value: value)
return smart as? T
case is CGFloat.Type:
let smart = compatibleCGFloatType(value: value)
return smart as? T
case 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润饰的目标有必要满意一下三个要求:
-
有必要遵循SmartDecodable协议
惯例的特点(Bool/String/Int等)没有运用 @SmartOptional 特点包装器的必要,因此做了该约束。
-
有必要是可选特点
非可选的特点没有运用 @SmartOptional 特点包装器的必要,因此做了该约束。
-
有必要是class类型
经过
didFinishMapping
修正该特点的值的状况下,借用 class 的引用类型的特性,达到修正的意图。能够检查 acceptChangesAfterMappingCompleted 该办法的完成。
为什么这么做?
这是一个不得已的完成计划。
- 为了做解码失利的兼容,咱们重写了KeyedEncodingContainer的decode和decodeIfPresent办法,这两个类型的办法均会走到兜底的smartDecode办法中。
该办法最终运用了public func decodeIfPresent(_ type: T.Type, forKey key: K) throws -> T? 完成了decode才能。
- KeyedEncodingContainer容器是用结构体完成的。 重写了结构体的办法之后,没办法再调用父办法。
- 这种状况下,假如再重写public func decodeIfPresent (*_ type: T.Type, forKey key: K) throws -> T?办法,就会导致办法的循环调用。
- 咱们运用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"
}