Swift JSON 教程:使用 JSON

来源:互联网 发布:centos怎么看版本 编辑:程序博客网 时间:2024/05/22 08:28

原文:Swift JSON Tutorial: Working with JSON
作者:Luke Parham
译者:kmyhy

2017-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 干净、可读行更高。

    • App.swift:一个简单的 Swift 结构,代表了 app。
    • DataManager.swift: 负责网络或本地数据的抓取。在本教程中我们会用这个文件中的方法来加载 JSON 数据。

你可以浏览一下 playground 中内容,再继续后面的内容!

原生 Swift JSON 解析

看完示例的 JSON 文件之后,我们可以来解析它并打印出排名第一的 app 是什么!

使用 optional 的初始化方法

首先,打开 App.swift。里面有一个简单的模型对象,定义了一个初始化方法,使用一个名字和一个链接作为初始化参数。

这个 JSON 文件的 entry 下面有一个对象数组,每个对象都代表了一个 App 对象。

根据这个结构,我们要在原来的初始化方法下面新增一个 optional 的初始化方法。

//1public 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。

使用能够抛出异常的初始化方法

在初始化失败时返回一个 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 集成到项目中

将 Gloss 集成到项目或 playground 中很简单:

  1. 从这里下载 Gloss 源代码 zip 包。
  2. 解压缩,然后将 Gloss-master/Sources 文件夹拖到 playround 的 Source 目录。

项目导航器看起来是这个样子:

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 UIKitimport PlaygroundSupportPlaygroundPage.current.needsIndefiniteExecution = trueURLCache.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()}
  1. 首先和之前一样,调用 JSONSerialization 反序列化数据。
  2. 然后,用这个 JSON 对象创建一个 TopApps。
  3. 最后,调用我们创建的模型对象的 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 抓取数据。代码解释如下:

  1. 首先调用 loadDataFromURL 方法,这个方法有一个 URL 参数和一个完成闭包,闭包有一个 Data 对象。
  2. 用一个可空绑定确保 data 不为空。
  3. 将 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 教程。有任何问题和建议,请在下面留言!

0 0
原创粉丝点击