美文网首页iOS开发笔记iOS Developer收藏swift
Swift 4.0 | JSON数据的解析和编码

Swift 4.0 | JSON数据的解析和编码

作者: 清無 | 来源:发表于2017-08-23 18:42 被阅读708次

    文 / 菲拉兔

    自己撸的图
    要求:
    • Platform: iOS8.0+
    • Language: Swift4.0
    • Editor: Xcode9
    【问题补充2017-09-28】

    最近我发现了一个问题:在Swift4.0中对JSON数据进行解析的时候,如果还用老的JSONSerialization类的话,会出现一个BUG:

    • 问题: 比如我有一个NSObject的类叫Student,其中包含一个var name = ""属性,那么在以上方法解析JSON数据的过程中,name的值将不被写入,这应该是Swift4.0的一个BUG;
    • 解决方法
      1. 用其他的名字替代name字段(暂时发现只有对这个属性不起作用),例如var sname = ""
      1. JSONDecoder新的方式去解析;

    Swift4.0以前和OC时代的JSON数据处理

    Swift(1..<4)& Objective-C

    Swift4.0以前的JSON解析/编码,和OC时代一样,都是通过NSJSONSerialization类的一些类方法进行处理的

    • JSON解析
    struct GroceryProduct{
        var name: String
        var points: Int
        var description: String
    }
    
    // 数据获取
    guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
        let data = try? Data.init(contentsOf: fileURL) else{
            fatalError("`JSON File Fetch Failed`")
    }
    
    // JSON序列化
    guard let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers), 
        let array = json as? [[String: Any]] else{
            fatalError("`JSON Data Serialize Failed`")
    }
    
    // 数据整理
    var products = [GroceryProduct]()
    for dict in array {
        products.append(GroceryProduct.init(name: dict["name"] as? String ?? "", 
                                            points: dict["points"] as? Int ?? 0, 
                                            description: dict["description"] as? String ?? ""))
    }
    print(products)
    

    Note:Swift编程中官方推荐用Struct代替Class,因为不占存储空间,但在实际开发中,如果用Struct去存储解析出来的JSON数据,还是比较麻烦的,尤其是在JSON序列化方面。下面用Class代替Struct演示Class在JSON序列化过程中的方便之处。

    // 数据解析和处理全封装在数据Model里,更体现封装性
    class GroceryProduct: NSObject{
        var name = ""
        var points = 0
        var descript = ""
        
        override func setValue(_ value: Any?, forKey key: String) {
            // 拦截并进行数据处理
            if key == "points" {
                points = (value as? Int) ?? 10
            }
            else{
                super.setValue(value, forKey: key)
            }
        }
        // 未定义key的处理
        override func setValue(_ value: Any?, forUndefinedKey key: String) {
            if key == "description" {
                descript = value as? String ?? ""
            }
        }
    }
    
    // 数据整理
    var products = [GroceryProduct]()
    for dict in array {
        let product = GroceryProduct()
        product.setValuesForKeys(dict)
        products.append(product)
    }
    print(products)
    
    • JSON编码
    // JSON编码
    struct GroceryProduct{
        var name: String
        var points: Int
        var description: String
    
        // 将对象中的属性-值转换为JSON字典
        func JSONDictionary(ignored keys: [String] = []) -> [String: Any] {
            var dictionary = [String: Any]()
            
            let mirror = Mirror.init(reflecting: self)
            for (key, value) in mirror.children {
                guard let key = key else{
                    continue
                }
                guard !keys.contains(key) else{
                    continue
                }
                dictionary.updateValue(value, forKey: key)
            }
            
            return dictionary
        }
    }
    
    // 需要编码的JSON Object
    var jsonArray = [[String: Any]]()
    for product in products {
        jsonArray.append(product.JSONDictionary())
    }
    
    // 判断是否是合法的JSON Object
    guard JSONSerialization.isValidJSONObject(jsonArray) else{
        fatalError("`Not Validate JSON Object`")
    }
    // 对象编码为JSON Data,并解析为JSON Text
    guard let data = try? JSONSerialization.data(withJSONObject: jsonArray, options: .prettyPrinted), 
        let jsonText = String(data: data, encoding: .utf8) else{
            fatalError("`JSON Object Encode Failed`")
    }
    print(jsonText)
    

    Note: 合法的JSON Object应满足:

    1. 顶层对象为数组或字典对象
    2. 数组/字典中的所有对象必须为字符串, 数字类型NSNumber, 数组, 字典, 或 NSNull
    3. 所有的字典keys为字符串
    4. 所有的数字对象不能为NaN 或 infinity
      所以struct / class 对象在JSON编码过程中需要自己手动转换成字典/数组,才可以正确被编码为JSON Data,并转换为字符串,然后发给服务器。

    Swift4.0中JSON的操作

    Swift4.0中利用全新采用JSONDecoder/JSONEncoder类来实现JSON数据的解析和编码。

    JSONDecoder

    • 要将JSON Data解析成相应的数据模型,并匹配相应的属性-值,对应的Struct或Class类型要遵守Decodable协议
    //Decodable只能解析,不能被编码
    struct GroceryProduct: Decodable{
        var name: String
        var points: Int
        var description: String
    }
    
    func swift4JSONParser() {
        // 数据获取
        guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
            let data = try? Data.init(contentsOf: fileURL) else{
                fatalError("`JSON File Fetch Failed`")
        }
        
    // 利用JSONDecoder来解析JSON Data,解析成[GroceryProduct].self数组类型
        let decoder = JSONDecoder()
        guard let products = try? decoder.decode([GroceryProduct].self, from: data) else{
            fatalError("`JSON Decode Failed`")
        }
        print(products)
    }
    

    Custom Key Names

    • 有些时候,服务器返回的JSON数据中的字段名采用“蛇形”命名法,如果要转成iOS中“驼峰”命名法,就要手动对keys做一次匹配。
    • Swift4.0中,只要指定Struct/Class中的CodingKeys并遵守CodingKeys协议枚举类型属性,并实现对应关系,就可以自动进行匹配替换解析。但注意如果CodingKeys中case没有匹配到JSON中的字段,解析就会失败。
    • 从这一点来说,还是挺麻烦的,不如用Class中的setValue(_ value: Any?, forUndefinedKey key: String),然后匹配指定对应的属性名称。
    struct GroceryProduct: Decodable{
        var name: String
        var points: Int
        var description: String
        
        // CodingKeys
        private enum CodingKeys: String, CodingKey{
            case name = "product_name"
            case points = "product_points"
            case description //保持一致,但必须实现所有属性
        }
    }
    
    func swift4JSONParser() {
        // 数据获取
        guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
            let data = try? Data.init(contentsOf: fileURL) else{
                fatalError("`JSON File Fetch Failed`")
        }
        
        let decoder = JSONDecoder()
        guard let products = try? decoder.decode([GroceryProduct].self, from: data) else{
            fatalError("`JSON Decode Failed`")
        }
        print(products)
    }
    

    Simple Nested JSON Data

    Swift4.0中对于JSON数据的嵌套结构解析,也有了新的方式,不过还是较为简单。

    • JSON数据:
    [
        {
            "name": "Home Town Market",
            "products": [
                {
                    "name": "Banana",
                    "points": 200,
                    "description": "A banana that's perfectly ripe."
                },
                {
                    "name": "Apple",
                    "points": 200,
                    "description": "A banana that's perfectly ripe."
                }
            ]
        }
    ]
    
    • 定义结构体
    struct Product: Decodable {
        var name: String
        var points: Int
        var description: String
    }
    
    struct GroceryStore: Decodable {
        var name: String
        var products: [Product]
    }
    
    • 嵌套解析
    func swift4JSONParser() {
        // 数据获取
        guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
            let data = try? Data.init(contentsOf: fileURL) else{
                fatalError("`JSON File Fetch Failed`")
        }
    
        // 会自动匹配解析成相应的Product对象,因为Product也实现了Decodable协议
        let decoder = JSONDecoder()
        guard let stores = try? decoder.decode([GroceryStore].self, from: data) else{
            fatalError("`JSON Decode Failed`")
        }
        print(stores)
    

    Multiple Level Nested JSON Data

    多层嵌套数据解析时,有一些结构是我们不需要存储的,这就我们定义一个中间的service模型来临时搭建这个结构。

    • JSON数据:
    [
        {
            "name": "Big City Market",
            "aisles": [
                {
                    "name": "Sale Aisle",
                    "shelves": [
                        {
                            "name": "Seasonal Sale",
                            "product": {
                                "name": "Chestnuts",
                                "points": 700,
                                "description": "Chestnuts that were roasted over an open fire."
                            }
                        },
                        {
                            "name": "Last Season's Clearance",
                            "product": {
                                "name": "Pumpkin Seeds",
                                "points": 400,
                                "description": "Seeds harvested from a pumpkin."
                            }
                        }
                    ]
                }
            ]
        }
    ]
    
    • 定义存储数据模型
    struct Product: Decodable {
        var name: String
        var points: Int
        var description: String
    }
    
    struct GroceryStore {
        var name: String
        var products: [Product]
    }
    
    // 中间`架构`类型
    struct GroceryStoreService: Decodable {
        let name: String
        let aisles: [Aisle]
        
        struct Aisle: Decodable {
            let name: String
            let shelves: [Shelf]
            
            struct Shelf: Decodable {
                let name: String
                let product: Product
            }
        }
    }
    
    // 扩展接口,实现数据解析
    extension GroceryStore {
        init(from service: GroceryStoreService) {
            name = service.name
            products = []
            
            for aisle in service.aisles {
                for shelf in aisle.shelves{
                    products.append(shelf.product)
                }
            }
        }
    }
    
    
    • 嵌套解析
    func swift4JSONParser() {
        // 数据获取
        guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
            let data = try? Data.init(contentsOf: fileURL) else{
                fatalError("`JSON File Fetch Failed`")
        }
        
        let decoder = JSONDecoder()
        guard let serviceStores = try? decoder.decode([GroceryStoreService].self, from: data)else{
            fatalError("`JSON Decode Failed`")
        }
        // 数据剥离存储
        let stores = serviceStores.map{ GroceryStore(from: $0) }
        print(stores)
    }
    

    Merge Data from Different Depths

    合并不同深度层的数据。此时一般要转换成KeyedDecodingContainer进行解析。

    • JSON数据:
    {
        "Banana": {
            "points": 200,
            "description": "A banana grown in Ecuador."
        },
        "Orange": {
            "points": 100,
            "description": "A juicy orange."
        }
    }
    
    • 数据模型:
    struct GroceryStore {
        struct Product {
            let name: String
            let points: Int
            let description: String
        }
    
        var products: [Product]
    
        init(products: [Product] = []) {
            self.products = products
        }
    }
    
    • 合并解析
    // 扩展增加ProductKey实现CodingKey,便于深层次解析属性
    extension GroceryStore {
        struct ProductKey: CodingKey {
    // 实现协议方法和属性
            var stringValue: String
            init?(stringValue: String) {
                self.stringValue = stringValue
            }
            
            var intValue: Int? { return nil }
            init?(intValue: Int) { return nil }
    
    // 自定义keys
            static let points = ProductKey(stringValue: "points")!
            static let description = ProductKey(stringValue: "description")!
        }
    }
    
    // 扩展实现Decodable协议,并通过decoder.container找到key对应的容器对象
    extension GroceryStore: Decodable{
        init(from decoder: Decoder) throws {
            var products = [Product]()
    // 找到包含ProductKey中的属性的所有容器
            let container = try decoder.container(keyedBy: ProductKey.self)
    
    // 然后遍历这个容器中的所有key,解析出容器中key对应的数据值
            for key in container.allKeys {
                let productContainer = try container.nestedContainer(keyedBy: ProductKey.self, forKey: key)
                let points = try productContainer.decode(Int.self, forKey: .points)
                let description = try productContainer.decode(String.self, forKey: .description)
                
                let product = Product(name: key.stringValue, points: points, description: description)
                products.append(product)
            }
            
            self.init(products: products)
        }
    }
    
    func swift4JSONParser() {
        // 数据获取
        guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
            let data = try? Data.init(contentsOf: fileURL) else{
                fatalError("`JSON File Fetch Failed`")
        }
        
        // 数据解析
        let decoder = JSONDecoder()
        guard let store = try? decoder.decode(GroceryStore.self, from: data)else{
            fatalError("`JSON Decode Failed`")
        }
        print(store.products)
    }
    

    JSONEncoder

    要实现包含Struct/Class对象的JSON对象的编码,在Swift4.0中较为简单,只需要遵守Encodable协议,并指定要编码的keys和实现协议encode方法即可。

    • JSON数据:
    [
        {
            "name": "Vegetables Store",
            "products": [
                {
                    "name": "Banana",
                    "points": 200,
                    "description": "A banana grown in Ecuador."
                },
                {
                    "name": "Orange",
                    "points": 100,
                    "description": "A juicy orange."
                }
            ]
        }
    ]
    
    • 编码实现
    struct Product: Decodable {
        let name: String
        let points: Int
        let description: String
    }
    
    struct GroceryStore: Decodable {
        var name: String
        var products: [Product]
    }
    
    // 实现编码协议
    extension GroceryStore: Encodable{
        private enum CodingKeys: CodingKey{
            case name
            case products
        }
        
    // 封装要编码的数据结构
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(name, forKey: .name)
            try container.encode(products, forKey: .products)
        }
    }
    
    extension Product: Encodable{
        private enum CodingKeys: CodingKey{
            case name
            case points
            case description
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(name, forKey: .name)
            try container.encode(points, forKey: .points)
            try container.encode(description, forKey: .description)
        }
    }
    
    // 要求object为实现了Encodable协议的对象
    func swift4JSONEncode<T: Encodable> (withJSONObject object: T){
        // 编码
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        guard let encodedData = try? encoder.encode(object), 
            let jsonText = String(data: encodedData, encoding: .utf8) else {
                fatalError("`JSON Encode Failed`")
        }
        print(jsonText)
    }
    
    补充2017-09-22

    在有些情况下,需要struct对象中的某些属性不是全部需要被存储和解析,就需要手动进行decode了

    • 定义结构体
    struct GitHubUser {
        var id: Int
        var type: String
        var loginName: String
        var avatarUrl: String
        var homepageUrl: String
        var profileUrl: String
        var name: String
        var company: String
        var location: String
        var blog: String
        var bio: String
        
        enum CodingKeys: String, CodingKey{
            case id,type,name,company,location,blog,bio
            case loginName = "login"
            case avatarUrl = "avatar_url"
            case homepageUrl = "html_url"
            case profileUrl = "url"
        }
    }
    
    • 自定义解析
    extension GitHubUser: Decodable{
        // 必须实现所有属性 - 初始值
        init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            id = try values.decode(Int.self, forKey: .id)
            type = try values.decode(String.self, forKey: .type)
            loginName = try values.decode(String.self, forKey: .loginName)
            avatarUrl = try values.decode(String.self, forKey: .avatarUrl)
            homepageUrl = try values.decode(String.self, forKey: .homepageUrl)
            profileUrl = try values.decode(String.self, forKey: .profileUrl)
            
            // 以下属性为可选解析的,设置默认值
            do {
                name = try values.decode(String.self, forKey: .name)
            }catch{
                name = ""
            }
            
            do {
                company = try values.decode(String.self, forKey: .company)
            }catch{
                company = ""
            }
            
            do {
                location = try values.decode(String.self, forKey: .location)
            }catch{
                location = ""
            }
            
            do{
                blog = try values.decode(String.self, forKey: .blog)
            }catch{
                blog = ""
            }
            
            do{
                bio = try values.decode(String.self, forKey: .bio)
            }catch{
                bio = ""
            }
        }
    }
    

    如果对你有帮助,别忘了点个❤️并关注下我哦。

    相关文章

      网友评论

        本文标题:Swift 4.0 | JSON数据的解析和编码

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