为什么将数据模块与网络模块一起编写?紧密相关的2者,也是应用程序最常见的组合:请求数据-存储数据-展示数据。
数据模块(JSON、Model、DB)
数据持久层其实有很多杂乱的选择,文件做为持久层的基础类型,又被分为sql、xml等等类型,我们可选的范围也很广阔,也可以自己动手来完善整个持久层模块,但这似乎对于很懒的程序员来说太浪费玩耍的时间了。所以我们索性在一些完善的开源库中挑选一类供我们做上层的封装。我们姑且确定选择FMDB作为我们的数据库支撑,但是联想到一般情况下,把JSON转换为Model直接存向数据库是常用需求,所以在数据模块这一层貌似扩展的很大了。我们首先引入SwiftyJSON作为JSON解析,不仅供框架使用也供外层使用,但是SwiftJSON解析后的数据仅是JSON对象,看来我们要亲自把JSON对象转换为Model了,写好之后,就是数据的存储。
通常情况下,iOS App中常用的存储:UserDefault、WriteToFile、SQL、钥匙串等。为了框架的通用性,只选择完善SQL,即Model->DB,至于别的存储让App在框架外层随意。如果你的团队不是足够成熟的话, 并不建议在项目中使用CoreData。
想想这个模块比较大,所以单开一个文件夹:Model吧。
首先引入SwiftJSON作为JSON解析的模块。
pod 'SwiftyJSON'
明确目标1:
- JSON对象转换为模型
- 模型转换为JSON对象
要实现我们这2个功能,必须遵守协议,在协议的方法中返回Mapping供我们赋值,类似:[属性名:JSON对象的Key]。
public protocol ModelMap {
/// 提供属性的对应Map
///
/// - returns: 字典,为:属性名称:(JSON对象中名称,类型)
static func keyMapping() ->Dictionary
}
首先我们要了解一些关于Swift runtime的东西,在纯Swift类中的属性在未添加dynamic修饰之前是不可以被运行时获取到属性的,之前我写过一篇有关Swift运行时的文章,可以细阅。
在这里有一个缺陷,就是我们这个库不允许你的模型是继承来的,因为static关键字代替了class final,在子类中不可以被覆盖,使用Extension又不能很好的解决方便程度上的问题而且需要写基类。所以在考虑了很多很多之后,还是决定写扩展!并且不对纯Swift类进行处理,只处理NSObject类。
extension NSObject {
/// 提供属性的对应Map
///
/// - returns: 字典,为:属性名称:JSON对象中名称
open class func keyMapping() ->Dictionary? {
return nil
}
/// 提供的类对应Map
open class func classMapping() ->Dictionary? {
return nil
}
/// 忽略的属性的名称
///
/// - returns: 数组,忽略的属性名称数组
open class func ignoredKey() ->[String]? {
return nil
}
}
这样所有需要被转换的类都需要继承Model类。但是使用的时候就比较尴尬了,必须要提供一个初始值,下面是我们的测试类:
import UIKit
import INSSwift
class Person: NSObject {
var name: String = ""
var gender : Bool = false
var array: Array = [""]
var dictionary: Dictionary = ["": ""]
var student: Student = Student()
var teachers: [Teacher] = []
var ignored: String = ""
override class func keyMapping() ->Dictionary? {
return [
"gender": "dictionary, gender",
"teachers": "student, t"
]
}
override class func classMapping() ->Dictionary? {
return [
"student": Student.self,
"teachers": Teacher.self
]
}
override class func ignoredKey() ->[String]? {
return ["ignored"]
}
}
class Student: NSObject {
var sname: String = ""
var sage: Int = 0
}
class Teacher: NSObject {
var tname: String = ""
}
如果dynamic var name: String?的话,后果自负…会提示错误不能转换为OC类型。这里我附上完整的Model处理的代码以及测试代码,在Model文件夹下新建文件Model.swift:
import Foundation
import SwiftyJSON
public enum PropertyType: String {
case tString = "T@\"NSString\""
case tNSArray = "T@\"NSArray\""
case tNSDictionary = "T@\"NSDictionary\""
case tBool = "TB"
case tClass = "T@\"_TtC"
case tDouble = "Td"
case tFloat = "Tf"
case tInt = "Tq"
case tInt8 = "Tc"
case tInt16 = "Ts"
case tInt32 = "Ti"
case tUInt = "TQ"
case tUInt8 = "TC"
case tUInt16 = "TS"
case tUInt32 = "TI"
case unknow = "UNKNOWTYPE"
}
extension NSObject {
/// 提供属性的对应Map
///
/// - returns: 字典,为:属性名称:JSON对象中名称
open class func keyMapping() ->Dictionary? {
return nil
}
/// 提供的类对应Map
open class func classMapping() ->Dictionary? {
return nil
}
/// 忽略的属性的名称
///
/// - returns: 数组,忽略的属性名称数组
open class func ignoredKey() ->[String]? {
return nil
}
}
/// 模型生成工厂
final public class ModelFactory {
// MARK: - 对象生成
/// 转换JSON->Model
///
/// - parameter json: JSON对象
/// - parameter cls: 类型
/// - returns: AnyObject
public class func Convert(JSON json: JSON?, to cls: AnyClass) ->AnyObject? {
let model = cls.alloc()
let keyMapping = cls.keyMapping()
let classMapping = cls.classMapping()
setValue(with: fullPropertyFor(class: cls), for: model, with: classMapping, with: keyMapping, data: json)
return model
}
// MARK: - Private methods
/// 赋值
/// - parameter props: 属性对应字典
/// - parameter object: 赋值的对象
/// - parameter clsMap: 类型映射字典
/// - parameter mapping: 属性映射字典
/// - parameter data: JSON对象
public class func setValue(with props: Dictionary?,
for object: AnyObject,
with clsMap: Dictionary?,
with mapping: Dictionary?,
data data: JSON?) {
guard let fullInfos = props, fullInfos.count != 0, let json = data else {
return
}
/// 将所有的Info取出来
for (key, typeString) in fullInfos {
/// 首先从Mapping中检测是否有对应的Mapper
let JSONKey = mapping?[key] ?? key
var JSONValue = json[JSONKey]
if JSONKey.contains(",") {
var items: Array = []
for item in JSONKey.components(separatedBy: ",") {
items.append(item.replacingOccurrences(of: " ", with: ""))
}
JSONValue = json[items]
}
let propType = PropertyType(rawValue: typeString)
if let classMapping = clsMap, let pointedClass = classMapping[key] {
// 该属性实现了类型Map
if propType == .tClass {
let sweetObject = Convert(JSON: JSONValue, to: pointedClass)
object.setValue(sweetObject, forKey: key)
continue
}
else if propType == .tNSArray {
let list: Array = JSONValue.arrayValue
var objectArray: Array = []
for item in list {
guard let arrayObject = Convert(JSON: item, to: pointedClass) else {
continue
}
objectArray.append(arrayObject)
}
object.setValue(objectArray, forKey: key)
continue
}
}
var value: Any? = JSONValue
if propType == .tString {
value = JSONValue.stringValue
}
if propType == .tInt || propType == .tInt8 || propType == .tInt16 || propType == .tInt32 ||
propType == .tUInt || propType == .tUInt8 || propType == .tUInt16 || propType == .tUInt32 {
value = JSONValue.intValue
}
if propType == .tDouble {
value = JSONValue.doubleValue
}
if propType == .tFloat {
value = JSONValue.floatValue
}
if propType == .tBool {
value = JSONValue.boolValue
}
if propType == .tNSArray {
value = JSONValue.arrayObject
}
if propType == .tNSDictionary {
value = JSONValue.dictionaryObject
}
object.setValue(value, forKey: key)
}
}
/// 获得子类与父类叠加后的属性列表
public class func fullPropertyFor(class cls: AnyClass) ->[String: String]? {
var currentCls: AnyClass = cls
var infoDict = [String: String]()
while let parent: AnyClass = currentCls.superclass() {
infoDict.merge(dict: propertyFor(class: currentCls))
currentCls = parent
}
return infoDict
}
/// 获取类的全部属性
public class func propertyFor(class cls: AnyClass) ->[String: String]? {
var count: UInt32 = 0
let properties = class_copyPropertyList(cls, &count)
let ignoredKeys = cls.ignoredKey() ?? []
var infoDict = [String: String]()
for index in 0..(dict: [K: V]?) {
guard let d = dict, d != nil, d.count != 0 else {
return
}
for (k, v) in d {
self.updateValue(v as! Value, forKey: k as! Key)
}
}
}
测试代码(测试的类在上边给出了):
let json: JSON = ["name": "Jack",
"ignored": "test ignored",
"array":
[
"1", "2",
"3", "4"
],
"dictionary":
[
"2": true,
"options": 666,
"gender": true
],
"student": [
"sname": "JACK",
"sage": 10,
"t": [
["tname": "teacher1"],
["tname": "teacher2"],
["tname": "teacher3"]
]
]
]
let object: Person = ModelFactory.Convert(JSON: json, to: Person.self) as! Person
// 使用filter答应、、
let s = object.teachers.filter {
let teacher: Teacher = $0
print(teacher.tname)
return true
}
// 在32行之前加一个断点查看对象属性!
ILog(.debug, object)
注:其实在实现Model提供Mapping的时候,期望还是使用Protocol的方式来做,但是不能解决static fun这个问题,static 代表着final class,所以会出现子类无法覆盖的问题。如果有解决方法还请告知,当然,这上边是我们自己手写的代码,在正真环境中,我还是选择了YYModel,前边仅是为了让大家了解一下转换代码。记得去掉SwiftJSON,等下我们的操作是Alamofire->YYModel->LKDBHelper,是不是很懒。
pod 'YYModel'
明确目标2:
- 模型-数据库中的操作(数据库的基本语法在这里不做介绍)
说到模型-数据库中的操作,其实特别想把LKDBHelper拿来直接使用,这里比较懒,因为代码其实和上边差不多,多个数据库操作而已,所以不写了。LKDBHelper是一套OC的代码而且在Swift上测试没有问题。但是他的Demo是4个月前更新的,应该是Swift2.3版本,所以我们决定,fork一份代码并切转换到Swift3.0做测试,如果没问题,直接拿来用,以减少我们框架模块的编写时间。
好了,测试没有问题,并且提交了PR。我们现在拉过来使用,作者目前还没有合并到master,大家可以先来我的仓库查看使用的Demo。
pod 'LKDBHelper'
这样我们JSON-模型-数据库的代码基本完成了30%。剩下的都是后期的针对业务的修改。
网络请求模块
使用Alamofire,写到这里,读者肯定说我这个框架没有什么技术含量;要知道,这些库的成长经历了多少人的洗礼,而我只有3个人的小团队,创业公司的项目压力各位应该知道。所以合理的使用第三方库也是一个好的选择!如果你在一个大的团队,那么就可以为了晋升去写一点东西了。知道原理就好了,知道原理不写的原因是没有人家那么考虑的全面、构思的缜密。
新建文件Request.swift,Request对象作为在网络层流动的对象,Alamofire作为支撑。
pod 'Alamofire'
但是你真的以为我会这么做么?这么做太麻烦了,我很懒的。我们使用Moya+Alamofire。直接上使用流程:可以选择继承INSRequest来实现自己的Request类,也可以直接下一个枚举统一管理,但是Moya这里有个问题是:实现Moya的协议必须要实现所有的方法,该协议中所有的属性全部是required(而且让我不理解的是,sampleData这种非必须数据竟然不是?,已经提了一个issue提问,因为文档没有很明确),对其中的一些属性做解释:
pod 'Moya'
记得把开发环境调整到iOS 9.0
baseURL: 根地址
path:网络请求路径
method:网络请求方法
parameters: 参数
sampleData:默认的Response data
task:当前任务的形态,一般为request,还有upload和download
import Foundation
import Moya
/// 在这里完成API的名称定义
enum MMAPI {
case launch
case signin(mobilePhoneNumber: String, password: String)
}
/// 在这里完成API所需信息补全
extension MMAPI: TargetType {
var baseURL: URL {
return URL(string: "http://leaf.leanapp.cn/api/")!
}
var path: String {
switch self {
case .launch:
return "launch.json"
case .signin:
return "login"
}
}
var method: Moya.Method {
switch self {
case .signin:
return .POST
default:
return .GET
}
}
var parameters: [String: Any]? {
switch self {
case .signin(let number, let password):
return ["phone": number, "passwd": password]
default:
return nil
}
}
var sampleData: Data {
return Data(base64Encoded: "")!
}
var task: Task {
return .request
}
}
在使用时候(顺便贴上官方Demo的地址):
let provider = MoyaProvider()
provider.request(.launch) { result in
switch result {
case let .success(response):
do {
if let json = try response.mapJSON() as? NSArray {
ILog(.debug, "\(json) \n \(response.statusCode)")
} else {}
} catch {}
case let .failure(error):
ILog(.debug, error.localizedDescription)
}
}
也是看到成功了,这个时候,我们把开始写的->Model->DB顺便测试一下:
if let json = try response.mapJSON() as? NSDictionary {
// 转换成对象
let launchModel = LaunchModel.yy_model(withJSON: json)
ILog(.debug, "\(launchModel?.imageUri) \n \(response.statusCode)")
// 存储到数据库
ILog(.debug, "Save to DB result \(launchModel?.saveToDB())")
// 查询
let searchedObject = LaunchModel.searchSingle(withWhere: nil, orderBy: nil) as! LaunchModel
ILog(.debug, "Searched result \(searchedObject.imageUri)")
}
⚒[DEBUG] [ViewController.swift: viewDidLoad(): 33]
Save to DB result Optional(true)
⚒[DEBUG] [ViewController.swift: viewDidLoad(): 36]
Searched result Optional("http://static.zhaogeshi.com")
这里有一个坑大家注意: 在Swift上使用LKDB的时候,必须重写返回类名的方法:
class LaunchModel: Model {
var bizUri: String?
var domains: NSArray?
var hotFixJS: String?
var imageUri:String?
var learningURL: String?
var status: String?
var version: String?
override static func getPrimaryKey() -> String {
return "version"
}
override static func getTableName() -> String {
return "LaunchModel"
}
}
否则,Swift这边读出的表名称会有问题,导致数据存储失败。
这样,借助第三方之手,我们美美的解决了领导给的2大块任务,数据模块与网络模块。依旧只能说,这2大功能模块只是提供了基础的功能,上层的封装还需要我们在接触业务的时候定制。所以这里只说,2大模块只完成了30%。
下一节开始写日志上报、异常捕获与处理、推送、位置、数据打点等。