快速入门
JSON(JavaScript Object Notation)是应用在 JavaScript 语言上的数据格式,常用于网络数据交换和存储。Apple 在Foundation
模块中集成了 JSON 格式数据的解析与生成方法,使得 JSON 数据可以快速简单地导入 Swift 代码和 Core Data 中。
本文将以一段 JSON 数据作为示例,着重讲述如何使用基类来处理 JSON 数据的导入。
JSON 数据分析
假设服务器返回了关于 Swift 商店的数据,包含了商店名称、商店地址、开业时间以及其分店信息:
{
"shopName": "Swift Shop",
"address": "2022 Apple Avenue",
"openYear": 2019,
"branches": [
{
"shopName": "Swift UI Experience",
"address": "2019 Apple Avenue",
"branch_manager": "Fiona"
},
{
"shopName": "Codable Protocol",
"address": "2014 iOS Street",
"branch_manager": "Steven",
"closed": true
}
]
}
可以看到这个 JSON 数据除了branch_manager
键,其他键命名均为小驼峰规范。第一层是由四个键值对组成的。在branches
下,第二层是一个数组并且数据由三个或四个键值对组成,键closed
是可选的,默认为false
。值得注意的是,第一层和第二层的结构中都有键shopName
和address
。
简单使用
如果不需要做任何优化或者没有进一步模型需求,那么有一个网站可以满足 JSON 转 Swift 对象的要求:QuickType。将该 JSON 数据复制到网站中即可生成对应的 Swift 模型代码和解析代码,还有其他高级选项可以进一步定制代码:
生成的代码如下:
// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
// let shopData = try? newJSONDecoder().decode(Shop.self, from: jsonData)
import Foundation
// MARK: - Shop
class Shop: Codable {
var shopName, address: String
let openYear: Int
let branches: [Branch]
init(shopName: String, address: String, openYear: Int, branches: [Branch]) {
self.shopName = shopName
self.address = address
self.openYear = openYear
self.branches = branches
}
}
// MARK: - Branch
class Branch: Codable {
let shopName, address, branchManager: String
let closed: Bool?
enum CodingKeys: String, CodingKey {
case shopName, address
case branchManager = "branch_manager"
case closed
}
init(shopName: String, address: String, branchManager: String, closed: Bool?) {
self.shopName = shopName
self.address = address
self.branchManager = branchManager
self.closed = closed
}
}
可以看到Branch
类中比Shop
类中多了一个enum CodingKeys...
结构。这是因为 Swift 中,默认生成或解析数据的变量命名为小驼峰方式,而Branch
中有一个branch_manager
键不满足要求,所以需要用CodingKey
协议下的枚举对这个键名进行映射,并且其他键名也需要在枚举中列出。
QuickType提供的功能已经可以满足大部分的需求了。如果不同层级下有许多键是重复的,这么做会使用大量的代码,既不美观又不够“聪明”。这种情况下,使用子类可以大大提高数据模型的拓展性,使用泛型处理数据会使代码变得更加轻巧。
进阶模型
根据上文的分析,可以先创建一个 BasicShop
类作为基类:
class BasicShop: Codable {
var shopName: String = ""
var address: String = ""
init() {}
}
与上文的初始化方法不同,基类中通过对变量赋初值来简化init()
函数。这一步对于子类的初始化是十分重要的。
根据上述定义的基类,先创建第一层级的模型Shop
:
class Shop: BasicShop {
var openYear: Int = 0
var branches: [Branch] = []
private enum CodingKeys: String, CodingKey {
case openYear
case branches
}
override init() {
super.init()
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
openYear = try container.decode(Int.self, forKey: .openYear)
branches = try container.decode([Branch].self, forKey: .branches)
try super.init(from: decoder)
}
override public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(openYear, forKey: .openYear)
try container.encode(branches, forKey: .branches)
try super.encode(to: encoder)
}
}
同样的,子类变量需要赋初值,并重写了init()
方法;在 Decoder 和 Encoder 的初始化方法中,还需要手动编码对应键值,并调用基类中Codable
协议对应的初始化方法。这里注意到有一处与网站生成的代码不同,子类中CodingKeys
枚举是必需的,因为在重写 Decoder 和 Encoder 时需要用到对应的映射。为了避免不必要的冲突,CodingKeys
枚举被设为private
私有的。
class Branch: BasicShop {
var branchManager: String
var closed: Bool?
private enum CodingKeys: String, CodingKey {
case branchManager = "branch_manager"
case closed
}
override init() {
super.init()
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
branchManager = try container.decode(String.self, forKey: .branchManager)
closed = try container.decodeIfPresent(Bool.self, forKey: .closed)
try super.init(from: decoder)
}
override public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(branchManager, forKey: .branchManager)
try container.encode(closed, forKey: .closed)
try super.encode(to: encoder)
}
}
值得注意的是,在解析closed
键时,使用的是container.decodeIfPresent
方法而不是container.decode
方法,后者在无法找到键值时会抛出错误,无法正常进行解析
解析和生成 JSON 数据
解析 JSON 数据:
let jsonData = jsonString.data(using: .utf8)
let shopData = try? JSONDecoder().decode(Shop.self, from: jsonData!)
生成 JSON 数据:
let jsonData = try JSONEncoder().encode(shopData)
let jsonString = String(data: jsonData, encoding: .utf8)
参考