项目上有一个需求,把一个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"
}
}
}
}
}
}
数了数,有好几个坑。
- 字典的key带有中文
- 字典的value类型不确定,可能是int, 可能是string,可能是数组,还有可能又是一个同样的字典,这个结构也许可以无限循环下去。
- 因为需求需要根据这个json来展示分级菜单,但菜单名存储在key里面,并且同级菜单不是存在一个数组里。
其实这个最好的解决方案是在字典里加一个name字段,然后把原来的字典改成数组。(无奈)
当然我看到这个问题首先肯定是去问领导,这个能不能让后端改一下json
然而被否决了。
而且所有的数据解析都要求用Model来处理,还不让直接用字典
这个字典转模型难度实在是有点大。
但是最终还是解决了
下面步骤:
- 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里去了
- 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】
- 不止要转换成功,还要把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)
}
}
}
这样,上面提到的坑全部都解决了。
网友评论