Swift 在 JSON解析方面有个比较有名的第三方库——SwiftyJSON,之前我也一直用的它。虽然用着还不错,但是它主要是为了避免手动解析 JSON 数据时大量的解包操作,降低解包不当导致 crash 的风险,感觉主要是注重安全性,易用性方面还是差了点。它支持下标操作,但毕竟是以字符串为键取值,IDE 不能自动补全,不仅麻烦还容易写错,而且用下标取到的值是JSON
类型,一般还需要再进行类型转换,终究没有操作一个 Model 来得方便。所以我一直想写一个 JSON-Model 的映射器,我想要的效果是这样的:
- 定义一个 Model :
class JSONModel {
var error = ""
var count = 0
var posts = []
}
- 发送网络请求后取得数据,然后直接转换成 JSONModel :
let jsonModel = data => JSOMModel.self
就这点需求,换个动态语言那根本不是事儿,哪怕在 C# 这样的静态语言中也能很简单地实现,只要用到反射特性就行了。然而 Swift 的反射实在是太弱了,运行期只能查看属性却不能给属性赋值。这简直无解,我读了一下 SwiftyJSON 的源码希望能找点灵感,然后发现 SwfityJSON 的流程是这样的(以直接解析 NSData 数据为例):
以一个 NSData 类型的数据作为构造器参数实例化一个 JSON(一个 struct ),在构造器中调用
NSJSONSerialization.JSONObjectWithData(...)
方法,如果 data 能被反序列化成一个AnyObject
类型的对象的话,就调用另一个构造器,把这个对象赋值给实例属性object
,否则就给object
赋一个NSNull()
。object
是一个计算属性,在给它赋值时,会对它的类型进行判断,然后把它的类型信息存储到实例属性type
中(type
是一个自定义的枚举类型,这个枚举类型基本对应了 Swift 中的几种基本类型),最后把object
的值进行类型转化后赋值给JSON
中的一个特定类型的私有属性,譬如是数组的话就赋值给rawArray
,是字符串的话就赋值给rawString
,等等。在获取
object
时会先判断实例属性type
,根据type
的值返回对应的 rawValue,譬如type == .String
的话,就返回rawString
。然后像array
和arrayValue
这样的都是计算属性,array
的话会先去判断type
是不是.Array
,是就返回rawArray
,否则返回nil
,而arrayValue
不会返回nil
,若类型不匹配则返回一个空数组[]
。
别的当然还有一些内容,譬如自定义下标,实现各种协议(字符串字面量协议、判等协议、比较协议、打印协议等等),代码很优雅,但似乎没有我想要的东西。
最终我觉得,用 Swfit 的原生语法应该是办不到了,只能借助于 OC 的 runtime。主要是要用到 KVC,这样一来所有的 Model 都得继承自 NSObject。我写了个 Demo,从联网获取数据到显示数据的整个流程如下:
- 先看看JSON数据的结构:
- 定义两个Model:
class JSONModel: NSObject {
var error = ""
var count = 0
var posts = []
}
class PostModel: NSObject {
var id = 0
var date = NSDate()
var name = ""
var pic = ""
var publishtime = ""
var count = 0
var excerpt = ""
}
- 发送网络请求(你可以使用 Alamore 或别的什么库,我这边是自己简单封装了一下 NSURLSession 直接用了),然后将取得的数据先转化成 JSONModel(直接使用
=>
符号),保存到实例属性jsonModel
中:
getDataFromUrl(Constant.DemoAPI, method: .GET, parameter: nil) { data, error in
if let jsonData = data {
self.jsonModel = jsonData => JSONModel.self
}
if let httpError = error {
print(httpError)
}
}
-
jsonModel
一旦被赋值就会刷新tableView
,看一下配置cell
的方法:
func configCell(cell: UITableViewCell, indexPath: NSIndexPath) -> UITableViewCell {
if let model = jsonModel, post = model.posts[indexPath.section] => PostModel.self {
cell.textLabel?.text = post.excerpt
}
return cell
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(Constant.ReuseIdentifier, forIndexPath: indexPath)
// Configure the cell...
return configCell(cell, indexPath: indexPath)
}
也是用=>
直接把posts
数组中的元素都转化为PostModel
类型的实例了,然后直接cell.textLabel?.text = post.excerpt
,就把我们想显示的内容放到cell
里了。
使用就是这么简单,只要新建一个NSObject
的子类,属性名保证跟 JSON 中的一致,并给各个属性一个初始值。如果想另取属性名也是可以的,用计算属性就好了,譬如 Demo 中 publishtime
是不符合 Swift 属性命名规范的,我们不去改原 Model,而是用一个扩展:
extension PostModel {
var publishTime: String {
return publishtime
}
}
这虽然不是很完美(因为publishtime
还在),但也凑合能用了。
转换器主要是用到了反射( Mirror 实现)和 KVC ,代码就不贴了,大家可以去 Github直接看源码,clone 下来跑一下 Demo 看看。要用到自己的项目中的话直接把 JSONModelMapper.swift
文件或者连同HTTPManager.swift
一起拖到项目中好了,因为实在“超轻量级”(简陋……),我觉得这样最方便了。
觉得还算有用的话随手点个 Star 可好……当然有什么意见或建议的话欢迎指教。源码在这里。