美文网首页iOS-swiftSwift学习iOS程序猿
Swift4中Codable的使用(三)

Swift4中Codable的使用(三)

作者: 砖头很烫手 | 来源:发表于2017-11-14 22:49 被阅读111次

    本篇是Swift4中Codable的使用系列第三篇,继续上一篇我们学习了如何自定义encode和decode,以及container的使用。本篇我们继续来了解更多Codable的知识。

    处理带有派生关系的模型

    在使用Codable进行json与模型之间转换,对于模型的类型使用struct是没什么问题,而类型是class并且是基类的话,同样也是没问题的,但是模型是派生类的话,则需要额外的处理,例如来看个小场景(这里的encode和decode方法均采用上一篇的泛型函数)

    class Ponit2D: Codable {
        var x = 0.0
        var y = 0.0
    }
    
    class Ponit3D: Ponit2D {
        var z = 0.0
    }
    
    let p1 = Ponit3D()
    try! encode(of: p1)
    let res = """
    {
        "x" : 1,
        "y" : 1,
        "z" : 1
    }
    """
    let p2 = try! decode(of: res, type: Ponit3D.self)
    dump(p2)
    

    接着我们来看看打印结果:

    {
      "x" : 0,
      "y" : 0
    }
    ▿ __lldb_expr_221.Ponit3D #0
      ▿ super: __lldb_expr_221.Ponit2D
        - x: 1.0
        - y: 1.0
      - z: 0.0
    

    咦?z去哪了???
    实际上,默认Codable中的默认encode和decode方法并不能正确处理派生类对象。因此,当我们的模型是派生类时,要自己编写对应的encode和decode的方法。
    首先我们先来实现encode:

    class Ponit2D: Codable {
        var x = 0.0
        var y = 0.0
        // 标记为private
        private enum CodingKeys: String, CodingKey {
            case x
            case y
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(x, forKey: .x)
            try container.encode(y, forKey: .y)
        }
    }
    
    class Ponit3D: Ponit2D {
        var z = 0.0
        // 标记为private
        private enum CodingKeys: String, CodingKey {
            case z
        }
        
        override func encode(to encoder: Encoder) throws {
            //调用父类的encode方法将父类的属性encode
            try super.encode(to: encoder)
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(z, forKey: .z)
        }
    }
    
    let p1 = Ponit3D()
    try! encode(of: p1)
    //{
    //  "x" : 0,
    //  "y" : 0,
    //  "z" : 0
    //}
    
    

    这里需要说明的是,CodingKeys需要用private标记,防止被派生类继承。其次,在encode方法中,我们要调用super.encode,否则父类的属性将没有进行编码,例如本例中若没有调用super.encode,encodePonit3D对象时则会只有z属性被编码,而x和y属性则不会。而调用super.encode时,我们直接把encoder传递给基类调用,因此基类和派生类共享一个container。当然你也可以为了区分他们单独创建一个container传递给父类。

    class Ponit3D: Ponit2D {
        var z = 0.0
        
        private enum CodingKeys: String, CodingKey {
            case z
        }
        
        override func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            // 创建一个提供给父类encode的容器来区分父类属性和派生类属性
            try super.encode(to: container.superEncoder())
            try container.encode(z, forKey: .z)
        }
    }
    
    let p1 = Ponit3D()
    try! encode(of: p1)
    //{
    //    "super" : {
    //        "x" : 0,
    //        "y" : 0
    //    },
    //    "z" : 0
    //}
    

    如果你不喜欢默认的super来做父类属性的key,也可以单独命名,container.superEncoder有一个forKey参数,通过CodingKeys的case来命名:

    class Ponit3D: Ponit2D {
        var z = 0.0
        
        private enum CodingKeys: String, CodingKey {
            case z
            case point2D //用于父类属性容器的key名
        }
        
        override func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            // 创建一个提供给父类encode的容器来区分父类属性和派生类属性,并将key设为point2D
            try super.encode(to: container.superEncoder(forKey: .Point2D))
            try container.encode(z, forKey: .z)
        }
    }
    
    let p1 = Ponit3D()
    try! encode(of: p1)
    //{
    //    "point2D" : {
    //        "x" : 0,
    //        "y" : 0
    //    },
    //    "z" : 0
    //}
    

    派生类encode的方法已经重写好了,接下来我们还要重写decode方法。其实decode方法和encode方法非常类似,通过init(from decoder: Decoder) throws方法调用super的方法,传递一个共享容器或则一个单独的容器就可以实现了,这里便不再演示了,有需要的可以查看本文的demo。


    model兼容多个版本的API

    假如有一个场景,一个app版本迭代,服务器对新版本的数据格式做了修改,例如有两个版本的时间格式:

    // version1
    {
        "time": "Nov-14-2017 17:25:55 GMT+8"
    }
    
    // version2
    {
        "time": "2017-11-14 17:27:35 +0800"
    }
    

    我们要根据版本的不同,上传给服务器的时间格式也不同,这里以encode为例,我们在Encoder的protocol中可以找到一个属性:

    public protocol Decoder {
        /// The path of coding keys taken to get to this point in encoding.
        public var codingPath: [CodingKey] { get }
    }
    

    我们可以使用这个userInfo属性在存储版本的信息,在encode的时候再读取版本信息来进行格式处理。而userInfo中的key是一个CodingUserInfoKey类型,CodingUserInfoKey和Dictionary中key的用法很类似。现在我们就有思路了,首先我们创建一个版本控制器来规定版本的信息:

    struct VersionController {
        enum Version {
            case v1
            case v2
        }
        
        let apiVersion: Version
        var formatter: DateFormatter {
            let formatter = DateFormatter()
            switch apiVersion {
            case .v1:
                formatter.dateFormat = "MMM-dd-yyyy HH:mm:ss zzz"
                break
            case .v2:
                formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
                break
            }
            return formatter
        }
        static let infoKey = CodingUserInfoKey(rawValue: "dateFormatter")!
    }
    

    接着我们修改调用的encode泛型函数,添加一个VersionController类型的参数用于传递版本信息:

    func encode<T>(of model: T, optional: VersionController? = nil) throws where T: Codable {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        if let optional = optional {
            // 通过userInfo存储版本信息
            encoder.userInfo[VersionController.infoKey] = optional
        }
        let encodedData = try encoder.encode(model)
        print(String(data: encodedData, encoding: .utf8)!)
    }
    

    然后我们来编写我们的模型:

    struct SomeThing: Codable {
        let time: Date
        
        enum CodingKeys: String, CodingKey {
            case time
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            // 通过userInfo读取版本信息
            if let versionC = encoder.userInfo[VersionController.infoKey] as? VersionController {
                let dateString = versionC.formatter.string(from: time)
                try container.encode(dateString, forKey: .time)
            } else {
                fatalError()
            }
        }
    }
    

    最后我们来验证我们的代码:

    let s = SomeThing(time: Date())
    
    let verC1 = VersionController(apiVersion: .v1)
    try! encode(of: s, optional: verC1)
    //{
    //    "time" : "Nov-14-2017 20:01:55 GMT+8"
    //}
    let verC2 = VersionController(apiVersion: .v2)
    try! encode(of: s, optional: verC2)
    //{
    //    "time" : "2017-11-14 20:03:47 +0800"
    //}
    

    现在我们已经通过Encoder中的userInfo属性来实现版本控制,对于decode只需在init方法对应实现即可。


    处理key个数不确定的json

    有一种总很特殊的情况就是我们得到这样一个json数据:

    let res = """
    {
        "1" : {
            "name" : "ZhangSan"
        },
        "2" : {
            "name" : "LiSi"
        },
        "3" : {
            "name" : "WangWu"
        }
    }
    """
    

    json中key的个数不确定,并且以学生的学号作为key,我们不能按照json的数据创建一个个的模型,对于这种情况我们又该如何处理?
    其实大致思路是这样的:我们同样创建一个包含id属性和name属性的Student模型,接着创建一个StudentList模型,StudentList中有一[Student]类型的属性用于存放Student模型。此时,我们知道系统默认Codable中的方法不能满足我们,我们需要自定义,而使用enum的Codingkeys来指定json中的key和属性的映射规则显然也不能满足我们,我们需要一个更灵活的Codingkeys,因此,我们可以使用上篇所提到的用struct类型实现Codingkeys,如果大家忘了话可以先倒回去看一遍其工作方式,这里就不再重复提了。

    struct Student: Codable {
        let id: Int
        let name: String
    }
    
    struct StudentList: Codable {
        var students: [Student] = []
        
        init(students: Student ... ) {
            self.students = students
        }
        
        struct Codingkeys: CodingKey {
            var intValue: Int? {return nil}
            init?(intValue: Int) {return nil}
            var stringValue: String //json中的key
            // 根据key来创建Codingkeys,来读取key中的值
            init?(stringValue: String) {
                self.stringValue = stringValue
            }
            // 相当于enum中的case
            // 其实就是读取key是name所应对的值
            static let name = Codingkeys(stringValue: "name")!
        }
    }
    

    现在我们有一个比较灵活的Codingkeys,我们接下来要做在decode中遍历container中所有key,因为key的类型是Codingkeys类型,所以我们可以通过key的stringValue属性来读取id,然后创建一个内嵌的keyedContainer来读取key对应的字典,然后再读取name的值,这就是大致的思路:

        init(from decoder: Decoder) throws {
            // 指定映射规则
            let container = try decoder.container(keyedBy: Codingkeys.self)
            var students: [Student] = []
            for key in container.allKeys { //key的类型就是映射规则的类型(Codingkeys)
                if let id = Int(key.stringValue) { // 首先读取key本身的内容
                    // 创建内嵌的keyedContainer读取key对应的字典,映射规则同样是Codingkeys
                    let keyedContainer = try container.nestedContainer(keyedBy: Codingkeys.self, forKey: key)
                    let name = try keyedContainer.decode(String.self, forKey: .name)
                    let stu = Student(id: id, name: name)
                    students.append(stu)
                }
            }
            self.students = students
        }
    

    测试一下代码验证时都正确:

    let stuList2 = try! decode(of: res, type: StudentList.self)
    dump(stuList2)
    //▿ __lldb_expr_752.StudentList
    //  ▿ students: 3 elements
    //    ▿ __lldb_expr_752.Student
    //      - id: 2
    //      - name: "LiSi"
    //    ▿ __lldb_expr_752.Student
    //      - id: 1
    //      - name: "ZhangSan"
    //    ▿ __lldb_expr_752.Student
    //      - id: 3
    //      - name: "WangWu"
    

    对于encode的方法,其实就是对着decode的反向来进行,我们只需要方向思考一下就很容易知道如何操作了:

        func encode(to encoder: Encoder) throws {
            // 指定映射规则
            var container = encoder.container(keyedBy: Codingkeys.self)
            try students.forEach { stu in
                // 用Student的id作为key,然后该key对应的值是一个字典,所以我们创建一个处理字典的子容器
                var keyedContainer = container.nestedContainer(keyedBy: Codingkeys.self, forKey: Codingkeys(stringValue: "\(stu.id)")!)
                try keyedContainer.encode(stu.name, forKey: .name)
            }
        }
    

    测试一下代码验证时都正确:

    let stu1 = Student(id: 1, name: "ZhangSan")
    let stu2 = Student(id: 2, name: "LiSi")
    let stu3 = Student(id: 3, name: "WangWu")
    let stuList1 = StudentList(students: stu1, stu2, stu3)
    try! encode(of: stuList1)
    //{
    //    "1" : {
    //        "name" : "ZhangSan"
    //    },
    //    "2" : {
    //        "name" : "LiSi"
    //    },
    //    "3" : {
    //        "name" : "WangWu"
    //    }
    //}
    
    

    Coable中错误的类型(EncodingError & DecodingError)

    在本系列的最后,我们来了解一下在Coable中会发生哪些错误。在编码和解码是会出现的错误类型是DecodingErrorEncodingError。我们先来看看DecodingError:

    public enum DecodingError : Error {
        // 在出现错误时通过context来获取错误的详细信息
        public struct Context {
            public let codingPath: [CodingKey]
            // 错误信息中的具体错误描述
            public let debugDescription: String
            public let underlyingError: Error?
            public init(codingPath: [CodingKey], debugDescription: String, underlyingError: Error? = default)
        }
        /// 下面是错误的类型
        // JSON值和model类型不匹配
        case typeMismatch(Any.Type, DecodingError.Context)
        // 不存在的值
        case valueNotFound(Any.Type, DecodingError.Context)
        // 不存在的key
        case keyNotFound(CodingKey, DecodingError.Context)
        // 不合法的JSON格式
        case dataCorrupted(DecodingError.Context)
    }
    

    相对DecodingErrorEncodingError的错误类型只有一个:

    public enum EncodingError : Error {
        // 在出现错误时通过context来获取错误的详细信息
        public struct Context {
            public let codingPath: [CodingKey]
            // 错误信息中的具体错误描述
            public let debugDescription: String
            public let underlyingError: Error?
            public init(codingPath: [CodingKey], debugDescription: String, underlyingError: Error? = default)
        }
        // 属性的值与类型不合符
        case invalidValue(Any, EncodingError.Context)
    }
    

    至此,本系列的教学就到此为止了,掌握了Codable的使用会为我们带来许多的便利,可以解决大多数情况的json数据。

    本文Demo

    相关文章

      网友评论

        本文标题:Swift4中Codable的使用(三)

        本文链接:https://www.haomeiwen.com/subject/iseqvxtx.html