swift 使用Codable 解析复杂类型

2021-03-31  本文已影响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)
        }
    }
}

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

上一篇 下一篇

猜你喜欢

热点阅读