美文网首页
swift 使用Codable 解析复杂类型

swift 使用Codable 解析复杂类型

作者: 树羽咕 | 来源:发表于2021-03-31 18:03 被阅读0次

    项目上有一个需求,把一个h5页面改造为原生app,但接口和json仍使用原来的。
    但json的格式对iOS非常不友好
    它的结构可能是这样的

    {
        "A": "呵呵",
        "B": "哈哈",
        "C": "CCC",
        "路人甲": "123"
    }
    

    这样的

    {
        "A": "呵呵",
        "B": ["哈哈"],
        "C": {
            "height": "10cm",
            "width": "5cm"
        },
        "路人甲": {
            "info": {
                "年龄": 18,
                "性别": "男"
            }
        }
    }
    

    甚至这样的

    {
        "A": "呵呵",
        "B": ["哈哈"],
        "C": {
            "height": "10cm",
            "width": "5cm"
        },
        "路人甲": {
            "info": {
                "年龄": 48,
                "性别": "男"
            },
            "children": {
                "路人乙": {
                    "info": {
                        "年龄": 22,
                        "性别": "男"
                    }
                },
                "路人丙": {
                    "info": {
                        "年龄": 28,
                        "性别": "女"
                    },
                    "children": {
                        "小明": {
                            "info": {
                                "年龄": 4,
                                "性别": "男"
                            }
                        }
                    }
                }
            }
        }
    }
    

    孩子那个看不懂?可以看看下面这个,也是类似的结构。

    {
        "一级菜单": {
            "code": "1",
            "data": {
                "二级菜单1": {
                    "code": "100",
                    "data": {
                        "三级菜单11": {
                            "code": "10000"
                        }
                    }
                },
                "二级菜单2": {
                    "code": "101",
                    "data": {
                        "三级菜单21": {
                            "code": "10100"
                        },
                        "三级菜单22": {
                            "code": "10101"
                        }
                    }
                }
            }
        }
    }
    

    数了数,有好几个坑。

    1. 字典的key带有中文
    2. 字典的value类型不确定,可能是int, 可能是string,可能是数组,还有可能又是一个同样的字典,这个结构也许可以无限循环下去。
    3. 因为需求需要根据这个json来展示分级菜单,但菜单名存储在key里面,并且同级菜单不是存在一个数组里。
      其实这个最好的解决方案是在字典里加一个name字段,然后把原来的字典改成数组。(无奈)

    当然我看到这个问题首先肯定是去问领导,这个能不能让后端改一下json
    然而被否决了。
    而且所有的数据解析都要求用Model来处理,还不让直接用字典
    这个字典转模型难度实在是有点大。

    但是最终还是解决了

    下面步骤:

    1. key带中文问题
      用JSONDecoder把字典转model,字典的key必须和model的属性一样,而属性不能用中文
      但我们可以使用CodingKeys更改他们的对应关系。
    struct AModel: Codable {
        var A: String?
        var B: String?
        var C: String?
        var someone: String?
        
        enum CodingKeys: String, CodingKey {
            case someone = "路人甲"
            case A
            case B
            case C
        }
    }
    

    这样在转换的时候 路人甲的数据就可以存到someone里去了

    1. value类型不确定
      这个我们可以使用枚举的高级用法——关联值(Associated Value)
      先上代码
    enum TestModelEnum: Codable {
        case int(Int)
        case string(String)
        case stringArray([String])
        case dictionary([String: String])
        case modelDict([String: TestModelEnum])
    
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            if let x = try? container.decode(Int.self) {
                self = .int(x)
                return
            }
    
            if let x = try? container.decode(String.self) {
                self = .string(x)
                return
            }
    
            if let x = try? container.decode([String].self) {
                self = .stringArray(x)
                return
            }
    
            if let x = try? container.decode([String: String].self) {
                self = .dictionary(x)
                return
            }
    
            if let x = try? container.decode([String: TestModelEnum].self) {
                self = .modelDict(x)
                return
            }
    
            throw DecodingError.typeMismatch(TestModelEnum.self,
                                             DecodingError.Context(codingPath: decoder.codingPath,
                                                                   debugDescription: "Wrong type for TestModelEnum"))
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            switch self {
            case .int(let x):
                try container.encode(x)
            case .string(let x):
                try container.encode(x)
            case .stringArray(let x):
                try container.encode(x)
            case .dictionary(let x):
                try container.encode(x)
            case .modelDict(let x):
                try container.encode(x)
            }
        }
    }
    

    我们把所有可能出现的类型都写在了case里,而且由于字典里还可能再嵌套字典,所以我的枚举类型里包含了自身
    在init方法中会去尝试转换成各种类型,转换失败会去尝试另一种,需要注意的是,如果你的类型是另一个Model, 而且这个Model的所有属性都是可选的(optional),但实际上你这里的数据可能只是一个String,转换会成功但Model里面的值都为nil【一定要失败才能继续往下进行,可以控制优先级或者去掉optional】

    1. 不止要转换成功,还要把key作为信息保存起来
      先从那个菜单的开始,那个结构相对简单一点
      我们先写一个基本类型,这是每级菜单里的信息
    struct MenuModel: Codable {
        var code: String?
        var data: [String: MenuModel]?
    }
    

    再写一个AllMenuModel用来转存MenuModel的数据

    struct AllMenuModel: Codable {
        var values: [MenuModel] = []
        var keys: [String] = []
    
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            if let x = try? container.decode([String: MenuModel].self) {
                for item in x {
                    keys.append(item.key)
                    values.append(item.value)
                }
                return
            }
    
            throw DecodingError.typeMismatch(MenuModel.self,
                                             DecodingError.Context(codingPath: decoder.codingPath,
                                                                   debugDescription: "Wrong type for MenuModel"))
        }
    }
    

    测试方法

        func test() {
            let json = [
                "一级菜单": [
                    "code": "1",
                    "data": [
                        "二级菜单1": [
                            "code": "100",
                            "data": [
                                "三级菜单11": [
                                    "code": "10000"
                                ]
                            ]
                        ],
                        "二级菜单2": [
                            "code": "101",
                            "data": [
                                "三级菜单21": [
                                    "code": "10100"
                                ],
                                "三级菜单22": [
                                    "code": "10101"
                                ]
                            ]
                        ]
                    ]
                ]
            ] as [String: Any]
            let model = try? JSONDecoder().decode(AllMenuModel.self, from: JSONSerialization.data(withJSONObject: json, options: []))
            print(model)
        }
    

    输出结果:

    Optional(DecoderTest.AllMenuModel(values: [DecoderTest.MenuModel(code: Optional("1"), data: Optional(["二级菜单2": DecoderTest.MenuModel(code: Optional("101"), data: Optional(["三级菜单22": DecoderTest.MenuModel(code: Optional("10101"), data: nil), "三级菜单21": DecoderTest.MenuModel(code: Optional("10100"), data: nil)])), "二级菜单1": DecoderTest.MenuModel(code: Optional("100"), data: Optional(["三级菜单11": DecoderTest.MenuModel(code: Optional("10000"), data: nil)]))]))], keys: ["一级菜单"]))
    

    转换成功了,并且key被我们存到了AllMenuModel里以供使用。
    当然,也可以写一个BaseModel基类,里面带一个属性key,这样key和value可以在同一个model里。
    我这里就不写了。

    这个问题解决了,和第二个解决方案组合一下就可以解决路人甲的那个问题了。
    但是要解决那个问题,又得写一个类似AllMenuModel的结构体,会不会太麻烦了。
    于是我对这里的代码做了一些改进。

    其实他们的区别只在于values的类型不一样,把类型做成泛型就好了。

    public struct KeyValueDictionary<T: Codable>: Codable {
        var values: [T] = []
        var keys: [String] = []
    
        public init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            if let x = try? container.decode([String: T].self) {
                for item in x {
                    keys.append(item.key)
                    values.append(item.value)
                }
                return
            }
    
            throw DecodingError.typeMismatch(KeyValueDictionary.self,
                                             DecodingError.Context(codingPath: decoder.codingPath,
                                                                   debugDescription: "Wrong type for KeyValueDictionary"))
        }
    }
    
    enum TestModelEnum: Codable {
        case int(Int)
        case string(String)
        case stringArray([String])
        case dictionary([String: String])
        case info(KeyValueDictionary<[String: TestModelEnum]>)
    
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            if let x = try? container.decode(Int.self) {
                self = .int(x)
                return
            }
    
            if let x = try? container.decode(String.self) {
                self = .string(x)
                return
            }
    
            if let x = try? container.decode([String].self) {
                self = .stringArray(x)
                return
            }
    
            if let x = try? container.decode([String: String].self) {
                self = .dictionary(x)
                return
            }
    
            if let x = try? container.decode(KeyValueDictionary<[String: TestModelEnum]>.self) {
                self = .info(x)
                return
            }
    
            throw DecodingError.typeMismatch(TestModelEnum.self,
                                             DecodingError.Context(codingPath: decoder.codingPath,
                                                                   debugDescription: "Wrong type for TestModelEnum"))
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            switch self {
            case .int(let x):
                try container.encode(x)
            case .string(let x):
                try container.encode(x)
            case .stringArray(let x):
                try container.encode(x)
            case .dictionary(let x):
                try container.encode(x)
            case .info(let x):
                try container.encode(x)
            }
        }
    }
    

    这样,上面提到的坑全部都解决了。

    相关文章

      网友评论

          本文标题:swift 使用Codable 解析复杂类型

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