动手写个 JSON-Model Mapping 库

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。然后像arrayarrayValue这样的都是计算属性,array的话会先去判断type是不是.Array,是就返回rawArray,否则返回nil,而arrayValue不会返回nil,若类型不匹配则返回一个空数组[]

别的当然还有一些内容,譬如自定义下标,实现各种协议(字符串字面量协议、判等协议、比较协议、打印协议等等),代码很优雅,但似乎没有我想要的东西。

最终我觉得,用 Swfit 的原生语法应该是办不到了,只能借助于 OC 的 runtime。主要是要用到 KVC,这样一来所有的 Model 都得继承自 NSObject。我写了个 Demo,从联网获取数据到显示数据的整个流程如下:

  • 先看看JSON数据的结构:
JSON.png
  • 定义两个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里了。

显示数据.png

使用就是这么简单,只要新建一个NSObject的子类,属性名保证跟 JSON 中的一致,并给各个属性一个初始值。如果想另取属性名也是可以的,用计算属性就好了,譬如 Demo 中 publishtime 是不符合 Swift 属性命名规范的,我们不去改原 Model,而是用一个扩展:

extension PostModel {
    var publishTime: String {
        return publishtime
    }
}

这虽然不是很完美(因为publishtime还在),但也凑合能用了。

转换器主要是用到了反射( Mirror 实现)和 KVC ,代码就不贴了,大家可以去 Github直接看源码,clone 下来跑一下 Demo 看看。要用到自己的项目中的话直接把 JSONModelMapper.swift文件或者连同HTTPManager.swift一起拖到项目中好了,因为实在“超轻量级”(简陋……),我觉得这样最方便了。

觉得还算有用的话随手点个 Star 可好……当然有什么意见或建议的话欢迎指教。源码在这里

推荐阅读更多精彩内容