原文:Swift JSON Tutorial: Working with JSON
作者:Luke Parham
译者:kmyhy2017-1-15 更新说明:本教程由 Luke Parham 更新为 Xcode 8.2 和 Swift 3。原文作者是 Attila Hegedüs。
JavaScript Object Notation,简称 JSON,是一种常用的和 web 服务进行数据传输的方式。它易于使用和阅读,因此使用者众多。
以下列 JSON 为例:
[
{
"person": { "name": "Dani","age": "24" } },{
"person": { "name": "Ray","age": "70" } }
]
在 O-C 中,解析这段 JSON 非常简单:
NSArray *json = [NSJSONSerialization JSONObjectWithData:JSONData options:kNilOptions error:nil];
NSString *age = json[0][@"person"][@"age"];
NSLog(@"Dani's age is %@",age);
在 Swift 中,要解析这段 JSON 就相当麻烦了,因为 Swift 有 optional 和类型安全的限制:
var json: [Any]?
do {
json = try JSONSerialization.jsonObject(with: data)
} catch {
print(error)
}
guard let item = json?.first as? [String: Any],let person = item["person"] as? [String: Any],let age = person["age"] as? Int else {
return
}
由于 guard 语句的存在,我们摆脱了厄运金字塔代码,但仍然需要些大量重复的代码才能访问 JSON 字符串中的数据。
在这篇 Swift JSON 教程里,我们一开始会用 Swift 的原生方法来解析 JSON——不使用任何第三方库。这实际上是苹果推荐的做法,因为 Swift 内置的工具在大部分场景下都够我们用了。
但是,就像每个人都会偶尔发出叛逆的声音,你也可以阅读本教程的下半部分,学习如何用 Gloss 框架节省你的时间。
注意:Gloss 只是众多 JSON 框架之一,但它却是一个好的设计模式的例子,我们会很好地证明这一点。
我们会用这两种方式对一个 JSON 文件进行解析,这个文件列出了美国 App 商店中上榜“25 个最流行的 app”的应用。
开始
因为学习 JSON 并不需要用户界面,因此我们所有的练习都是在 playground 中进行的。请在这里下载开始项目。
打开 Swift.playground 。
注意:你可能发现 playground 的项目导航器是关闭的。如果是这样,请用 command+1 打开它。
开始项目中有几个源文件和资源文件,这是为了将你的注意力集中在用 Swift 解析 JSON 的主题上来。看一下项目结构,以了解大概的内容:
Resources 文件夹中包含了你要解析的示例 JSON 文件。
- topapps.json: 包含了 JSON 字符串。
Sources 文件夹中包含在主 playground 代码中能够调用的其它 Swift 源文件。将这些源文件放在 Sources 文件夹中是为了让你的 playground 干净、可读行更高。
你可以浏览一下 playground 中内容,再继续后面的内容!
原生 Swift JSON 解析
看完示例的 JSON 文件之后,我们可以来解析它并打印出排名第一的 app 是什么!
使用 optional 的初始化方法@H_301_140@
首先,打开 App.swift。里面有一个简单的模型对象,定义了一个初始化方法,使用一个名字和一个链接作为初始化参数。
这个 JSON 文件的 entry 下面有一个对象数组,每个对象都代表了一个 App 对象。
根据这个结构,我们要在原来的初始化方法下面新增一个 optional 的初始化方法。
//1
public init?(json: [String: Any]) {
//2
guard let container = json["im:name"] as? [String: Any],let name = container["label"] as? String,let id = json["id"] as? [String: Any],let link = id["label"] as? String else {
return nil
}
//3
self.name = name
self.link = link
}
这个初始化方法使用一个 [String:Any] 字典作为参数,这个字典用 JSON 数据来构造。这是可以的,有效的 JSON 要么是数组要么是单个值,但是我们已经确定它就是一个字典。
然后,用 guard-let 语法去解析 JSON,取出其中的 name 和 link 值。
最后,如果所有值都不为空,则填充模型对象的属性并返回模型。
创建好模型之后,在项目导航器中点击 TopApps-Starter,打开主 playground 文件。
首先,在 getTopAppsDataFromFileWithSuccess 方法的 success 块中添加 guard 语句:
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
PlaygroundPage.current.finishExecution()
}
这里,我们用 jsonObject(with:options:) 方法将 JSON 字符串转换为 JSON 对象。
然后,我们进行第一步解析,获得代表第一个 App 的 JSON 对象。
guard let Feed = json?["Feed"] as? [String: Any],let apps = Feed["entry"] as? [[String: Any]],let firstApp = apps.first else {
PlaygroundPage.current.finishExecution()
}
这一步,我们首先取得最外层的 Feed 对象,它的 entry 键中包含了 app 数组。
最后,调用我们早先写好的 optional 的初始化函数获得排名第一的 app:
let app = App(json: firstApp)
print(app ?? "Failed to initialize")
运行 app,控制台输出如下:
App(name: "Game of War - Fire Age",link: "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2")
好了—— “Game of War – Fire Age” 就是在这个 JSON 文件中排名第一的 app。
使用能够抛出异常的初始化方法@H_301_140@
在初始化失败时返回一个 nil 而不是实例对象,并不能告诉我们到底发生了什么错误。
打开 App.swift ,定义一个错误枚举。
enum SerializationError: Error {
case missing(String)
}
然后,将 optional 初始化方法换成下面的初始化方法。当 JSON 参数中包含有空值时,这个初始化方法会抛出一个上面定义的错误。
public init(json: [String: Any]) throws {
//1
guard let container = json["im:name"] as? [String: Any],let name = container["label"] as? String else {
throw SerializationError.missing("name")
}
guard let id = json["id"] as? [String: Any],let link = id["label"] as? String else {
throw SerializationError.missing("link")
}
//2
self.name = name
self.link = link
}
这里,我们为每个属性使用一个单独的 guard 语句。当传入的 JSON 参数中包含无效的值时,抛出一个该属性未找到的错误。
最后,如果没有任何异常发生,我们填充属性,返回有效的实例对象。
打开主 playground 将这 2 句:
let app = App(json: firstApp) print(app ?? "Failed to initialize")
替换为:
do {
let app = try App(json: firstApp)
print(app)
} catch let error {
print(error)
}
这里需要使用 do-catch 块。如果初始化成功,我们不需要担心 app 会出现控制。如果初始化失败,我们会得到一个有用的错误信息,而不是一个空对象。
要查看初始化失败的效果,请将 App(json: firstApp) 换成 App(json: [:])。
用 Gloss 解析 JSON
你已经体验了如果将 JSON 解析成自己的模型对象,接下来我们来尝试另一种解析方法。
为了保持美观、大方,我们创建一个新的 playground 叫做 Gloss.playground。然后,将 topapps.json 拷贝到 Resources 目录,将 DataManager.swift 拷贝到 Source。
将 Gloss 集成到项目中@H_301_140@
将 Gloss 集成到项目或 playground 中很简单:
项目导航器看起来是这个样子:
https://koenig-media.raywenderlich.com/uploads/2017/01/Screen-Shot-2017-01-16-at-12.56.00-AM-411x500.png’ width=’300’/>这就好了!下载可以在我们的 playground 中使用 Gloss 了,这是一种“简单”的 JSON 解析方法!
将 JSON 映射为对象
首先,必须定义模型对象和 JSON 对象的映射方式。
模型对象必须实现 Decodeable 协议,这个协议允许它们从 JSON 进行解码。要实现这个协议,需要实现 init?(json: JSON) 初始化方法。
注意:打开 Gloss.swift。在 Decodable 协议中,如果用 ⌘+左键点击 JSON 查看它的定义,你会看到它仅仅是在同一个文件中被定义为 [String: Any] 的别名。
TopApps
TopApps 模型用于表示顶级对象,它只包含一个键值对:
{
"feed": {
...
}
}
创建一个新的 Swift 文件,名为 TopApps.swift,保存到 playground 的 Sources 文件夹下。编辑它的代码为:
public struct TopApps: Decodable {
// 1
public let feed: Feed?
// 2
public init?(json: JSON) {
feed = "feed" <~~ json
}
}
首先要定义模型所用到的属性。这里我们只有一个属性。先别管 Feed 报出的异常,我们会在后面定义 Feed 模型类。
为了实现 Decodable 协议,TopApps 必须实现 optional 的初始化方法。
可能 <~~ 运算符有点陌生。它是 Encode 运算符,在 Gloss 的 Operators.swift 文件中定义。因为 Feed 也是一个 Decodable 类型,Gloss 可以把编码工作交给这个对象。
Feed
Feed 对象和顶级对象很像。它有两个键值对,但由于我们只对上榜的 25 个 app 感兴趣,因此 author 对象其实是没有必要处理的。
{
"author": {
...
},"entry": [
...
]
}
新建一个 Swift 文件,名为 Feed.swift,保存到 Sources 文件夹下:
public struct Feed: Decodable {
public let entries: [App]?
public init?(json: JSON) {
entries = "entry" <~~ json
}
}
App
App 是最后一个模型对象,它表示一个 app 对象:
{
"im:name": {
"label": "Game of War - Fire Age"
},"id": {
"label": "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2",...
},...
}
新建 App.swift 文件,保存到 Sources 文件夹下,编辑它的代码为:
public struct App: Decodable {
// 1
public let name: String
public let link: String
public init?(json: JSON) {
// 2
guard let container: JSON = "im:name" <~~ json,let id: JSON = "id" <~~ json else {
return nil
}
guard let name: String = "label" <~~ container,let link: String = "label" <~~ id else {
return nil
}
self.name = name
self.link = link
}
}
Feed 和 TopApps 都使用 optional 属性。但当我们确定 JSON 中某个值肯定存在时,可以用非 optional 的属性。
我们并不需要为 JSON 的每个成员都创建一个模型对象。例如,这里就没有为 in:name 和 id 创建模型对象。当我们使用非可空对象和嵌套对象时,一定要进行非空校验。
现在模型类已经准备好了,我们该让 Gloss 干活了!
打开 playground 文件,将它的内容替换为:
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
URLCache.shared = URLCache(memoryCapacity: 0,diskCapacity: 0,diskPath: nil)
DataManager.getTopAppsDataFromFileWithSuccess { (data) -> Void in
// 1
var json: Any
do {
json = try JSONSerialization.jsonObject(with: data)
} catch {
print(error)
PlaygroundPage.current.finishExecution()
}
guard let dictionary = json as? [String: Any] else {
PlaygroundPage.current.finishExecution()
}
// 2
guard let topApps = TopApps(json: dictionary) else {
print("Error initializing object")
PlaygroundPage.current.finishExecution()
}
// 3
guard let firstItem = topApps.feed?.entries?.first else {
print("No such item")
PlaygroundPage.current.finishExecution()
}
print(firstItem)
PlaygroundPage.current.finishExecution()
}
- 首先和之前一样,调用 JSONSerialization 反序列化数据。
- 然后,用这个 JSON 对象创建一个 TopApps。
- 最后,调用我们创建的模型对象的 feed 属性的 entries 属性,获得第一个 app.
这就是全部我们需要做的工作!
保存文件,可以看到我们成功地拿到了 app 名字,但这次的办法要优雅得多了:
App(name: "Game of War - Fire Age",link: "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2")
注意:如果在这里出现错误,说 topapps.json 不能打开,那可能是没有权限,请关闭开始项目,然后删除 derived data 文件夹中的内容。
我们已经知道如何解析本地数据——要怎样解析远程数据呢?
抓取远程 JSON
让这个项目更完美些。通常我们需要抓取远程数据而不是本地文件。我们可以用一个网络请求抓取 App Store 的排行榜。
打开 DataManager.swift ,在 DataManager 的实现之前定义一个 topAppURL:
let topAppURL = "https://itunes.apple.com/us/rss/topgrossingipadapplications/limit=25/json"
然后,在实现部分添加这个方法:
public class func getTopAppsDataFromItunesWithSuccess(success: @escaping ((_ iTunesData: Data) -> Void)) {
//1
loadDataFromURL(url: URL(string: topAppURL)!) { (data,error) -> Void in
//2
if let data = data {
//3
success(data)
}
}
}
这个方法和之前的方法很相似,但这次使用了 URLSession 从 iTunes 抓取数据。代码解释如下:
- 首先调用 loadDataFromURL 方法,这个方法有一个 URL 参数和一个完成闭包,闭包有一个 Data 对象。
- 用一个可空绑定确保 data 不为空。
- 将 data 传递到 success 闭包,和之前一样。
打开主 playground 文件,将这句 :
DataManager.getTopAppsDataFromFileWithSuccess { (data) -> Void in
替换为:
DataManager.getTopAppsDataFromItunesWithSuccess { (data) -> Void in
现在你可以从 iTunes 获得数据了。
保存文件,你可以查看当前最热门的游戏是什么了。我看到的仍然是 “Saga 糖果消除”。我真的喜欢糖果消除游戏。
App(name: "Candy Crush Saga",link: "https://itunes.apple.com/us/app/candy-crush-saga/id553834731?mt=8&uo=2")
你看到的可能和上面不同,因为苹果商店里的排行榜随时都在变。
通常人们不仅仅对排行榜的第一名感兴趣——他们会想了解整个排行榜的内容。你没有必要为此写码——只需要用这句:
topApps.feed?.entries
Gloss 的底层机制
如你所见,在解析 JSON 数据时 Gloss 很好用,但它的底层机制是怎样的呢?
<~~ 是一个自定义操作符,用来执行一系列 Decoder.decode 函数。Gloss 内置了对许多类型的解码支持:
- 简单类型 (Decoder.decode(key:))
- Decodable 模型对象 (Decoder.decode(decodableForKey:))
- 简单数组 (Decoder.decode(key:))
- Decodable 模型数组 (Decoder.decode(decodableArrayForKey:) )
- 枚举类型 (Decoder.decode(enumForKey:))
- 枚举数组 (Decoder.decode(enumArrayForKey:) )
- URL 类型 (Decoder.decode(urlForKey:) )
- URL 数组 (Decode.decode(urlArrayForKey:))
在本教程中,你严重依赖 Decodable 模型。如果你需要更复杂的对象,你可以扩展 Decoder 并实现你自己的解码功能。
当然 Gloss 还能将对象转换回 JSON。如果你想了解这部分内容,请参考 Encodable 协议。
结束
这里下载完成的 playground 项目。
如果你更愿意采用本文前半部分描述的苹果的解析方式,你可以看一下苹果的在 Swift 中使用 JSON。它最终演示了一个如何编写使用 JSON 数据的网络层的实例非常有用。
但是,如果你更喜欢 Gloss 的方法,则需要密切关注它的新版本发布,因为它还在开发阶段。
希望你喜欢本教程,也请阅读本站的其它 Swift 教程。有任何问题和建议,请在下面留言!