SwiftSwift学习iOS Swift && Objective-C

Swift4 JSON 解析

2017-08-08  本文已影响638人  bewils

Swift 的 JSON 解析一直是一件很麻烦的事, 在 Swift3 中请求一个数据后可能要进行如下操作(比如服务器返回一个数组):

if let jsonObject = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) {
  if let objectList = jsonObject as? [Any] {
    for object in objectList {
      if let object = object as? [String: Any] {
        // use object
      }
    }
  }
}

经过千辛万苦终于拿到了这个 object , 然而噩梦才刚刚开始

转成 [String: Any] 的 object 基本上没法用, 很多时候我们还需要去拿每个 key 和 value 再赋给我们自己定义好的 struct / class (又是一长串的 if let as)

当然, 目前解决这个问题还是有一些办法的, 比如可以去尝试使用 SwiftyJSON 直接当原生的 Array / Dictionary 用, 或者更高级的 Argo, 直接将 JSON data 映射到自己定义的 struct

在 Swift4 中, Swift Standard Library 带来的新的类和协议支持原生的 JSON 解析. 甚至不只是 JSON, 只要是 encoding / decoding 的转化都可以支持(比如 plist)

关于 Swift4 Codable

在 Swfit4 中新添加了一个复合协议 typealias Codable = Decodable & Encodable, 想进行 encoding / decoding 只要实现这个协议即可. 而且根据需要, 如果只是从服务器取数据或只是向服务器发数据完全可以只实现其中一个, 接下来先来试验下 Decodable 协议. 先找一个 API, 就随便找了一个项目直接用 github 的 API https://api.github.com/repos/bewils/IWantTheGreenOne. 首先可以看看返回的数据结构: 因为只取了一个 repository 所以可以看到返回的结构也很简单, 按照这个结构写出如下 struct

struct Repo: Decodable {
  var `private`: Bool
  var html_url: String
  var description: String?
}

这里只取了其中的三个属性, 即只解析返回的 data 的这三个字段, 然后尝试发送请求并解析返回的数据

if let url = URL(string: "https://api.github.com/repos/bewils/IWantTheGreenOne") {
  let session = URLSession(configuration: .default)
  session.dataTask(with: url) { (data, _, err) in
    guard err == nil else { return }
    
    guard let data = data else { return }
    if let repo = try? JSONDecoder().decode(Repo.self, from: data) {
      print(repo)
    } else {
      print("JSON parse failed")
    }
  }.resume()
}

奇迹出现了, 很快控制台里就输出了 Repo(private: false, html_url: "https://github.com/bewils/IWantTheGreenOne", description: Optional("SpriteKit Game")) 这样的文字. 我们几乎没做什么事, 一如既往的网络请求, 在回调函数中利用同样是 Swift4 中新加入的 JSONDecoder 类来按照 Repo 的模型解析 data, 然后就成功的输出了解析结果

Codable 便利的一点还在于 Swift 中给出了这个协议的默认实现, 就像 Repo 直接遵从 Decodable 没有写解析的任何方法就能通过 JSONDecoder 解析出来

同时在声明 Repo 时我们将 description 声明成了 String? 的类型, 这样如果返回的数据里没有这个字段只会解析出 nil 而不会报错

关于字段名

第一个字段 private 是 Swift 的关键字, 当做变量名的时候只能用 `` 包起来, 那要不换个名字改成 jurisdiction. 运行, 好的, 报错: 没有 jurisdicition 这个字段

这只是一种情况, 还有比如返回的 JSON 的 key 是用 _ 分割的命名, 而 Swift 的代码风格一般是驼峰命名, 这时就会有字段名不对应的问题, 为了解决这个问题, 可以在 Repo 的内部声明一个遵从 CodingKey 的叫 CodingKeys 枚举值

struct Repo: Decodable {
  var jurisdiction: Bool
  var htmlUrl: String
  var description: String?
  
  enum CodingKeys: String, CodingKey {
    case jurisdiction = "private"
    case htmlUrl = "html_url"
    case description
  }
}

重新运行, 又一次成功地解析出了 Repo: Repo(jurisdiction: false, htmlUrl: "https://github.com/bewils/IWantTheGreenOne", description: Optional("SpriteKit Game")).

通过使用 CodingKey 的方式重新定义 JSON key 和属性名的对应关系是很好用的, 但这里还有一个小问题, 就是如果使用 CodingKey 就必须吧所有的属性和 key 都写进去, 比如 Repo 中虽然 description 不需要转化还是要写进去, 否则这次会找不到 description 对应的 key (如果有 50 个属性的对象为了一个属性而使用 CodingKeys 也是很惨的...)

关于嵌套结构

可以看到从 API 返回的数据虽然是基本的 Dictionary 结构, 但里面还是有一个 owner 的字段属于嵌套的结构, Swift4 中解析嵌套结构的方法非常简单: 也直接嵌套一个就可以了

struct Owner: Decodable {
  var login: String
  var id: Int
  var avatar_url: String
}

struct Repo: Decodable {
  var jurisdiction: Bool
  var htmlUrl: String
  var description: String?
  var owner: Owner
  
  enum CodingKeys: String, CodingKey {
    case jurisdiction = "private"
    case htmlUrl = "html_url"
    case description
    case owner
  }
}

新添加了一个 Owner 的 struct, 然后加到 Repo 中并且添加一个 CodingKey

运行, 输出: Repo(jurisdiction: false, htmlUrl: "https://github.com/bewils/IWantTheGreenOne", description: Optional("SpriteKit Game"), owner: __lldb_expr_36.Owner(login: "bewils", id: 16081099, avatar_url: "https://avatars3.githubusercontent.com/u/16081099?v=4"))

(简直完美, 此处应有掌声 �👏

关于数组

通过上面的过程已经可以将 Repo 成功解析出来了, 无论多复杂的 JSON, 只要是 key-value, 无论嵌套多深都是一样的写法

然而如果返回的是一个数组呢? https://api.github.com/users/bewils/repos

关于数组的解析将会分为两部分: 二逼程序员和文艺程序员

二逼程序员

再声明一个 stuct

struct UserRepos {
  var repoList: [Repo]
}

因为返回的是一个数组, 没有 key 所以通过默认方法肯定转换不来, 自定义去实现 Decodable

extension UserRepos: Decodable {
  init(from decoder: Decoder) throws {
    repoList = []
    
    var values = try decoder.unkeyedContainer()
    while !values.isAtEnd {
      let repo = try values.decode(Repo.self)
      repoList.append(repo)
    }
  }
}

自定义实现的过程中首先通过 decoder 取出 unkeyedContainer 即初步解析为数组型的结构, 如果这时输出的话可以看到 values 就是我们要的数组, 然后顺手开始 for in, values 的类型是 UnkeyedDecodingContainer, 但竟然不遵从 Sequence 协议…没办法遍历这可是个难题, 而且查看 values 的其他属性 isAtEnd = false, count = 31 也证明了 values 应该是可以遍历的

最后发现 UnkeyedDecodingContainer 的遍历方法就像一个队列, 每次调用 decode 就出一个, 然后通过 isAtEnd 来终止循环就可以了

if let url = URL(string: "https://api.github.com/users/bewils/repos") {
  let session = URLSession(configuration: .default)
  session.dataTask(with: url, completionHandler: { (data, _, err) in
    guard err == nil else { return }
    
    guard let data = data else { return }
    do {
      // 二逼程序员解析法
      let repos = try JSONDecoder().decode(UserRepos.self, from: data)
      print(repos.repoList)
    } catch let err {
      print(err)
    }
  }).resume()
}

这样就通过自定义实现 Decodable 成功地解析出了返回的数组

文艺程序员

文艺程序员的解析方法呢?

let repos = try JSONDecoder().decode([Repo].self, from: data)

最后

Swift4 中提供的 Codable 使得解析 JSON 变的极其方便, 这篇文章中主要讨论了 Decodable, 关于 Encodable 基本上就是 Decodable 的反向操作, 就不在这里讨论了

本文的 demo 代码放在如下地址 SwiftCodable

上一篇下一篇

猜你喜欢

热点阅读