Swift语法熟悉(三)

为什么将数据模块与网络模块一起编写?紧密相关的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解析的模块。

1
pod 'SwiftyJSON'

明确目标1:

  • JSON对象转换为模型
  • 模型转换为JSON对象

要实现我们这2个功能,必须遵守协议,在协议的方法中返回Mapping供我们赋值,类似:[属性名:JSON对象的Key]。

1
2
3
4
5
6
public protocol ModelMap {
/// 提供属性的对应Map
///
/// - returns: 字典,为:属性名称:(JSON对象中名称,类型)
static func keyMapping() ->Dictionary<String, (String, String)>
}

首先我们要了解一些关于Swift runtime的东西,在纯Swift类中的属性在未添加dynamic修饰之前是不可以被运行时获取到属性的,之前我写过一篇有关Swift运行时的文章,可以细阅。

在这里有一个缺陷,就是我们这个库不允许你的模型是继承来的,因为static关键字代替了class final,在子类中不可以被覆盖,使用Extension又不能很好的解决方便程度上的问题而且需要写基类。所以在考虑了很多很多之后,还是决定写扩展!并且不对纯Swift类进行处理,只处理NSObject类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extension NSObject {
/// 提供属性的对应Map
///
/// - returns: 字典,为:属性名称:JSON对象中名称
open class func keyMapping() ->Dictionary<String, String>? {
return nil
}
/// 提供的类对应Map
open class func classMapping() ->Dictionary<String, AnyClass>? {
return nil
}
/// 忽略的属性的名称
///
/// - returns: 数组,忽略的属性名称数组
open class func ignoredKey() ->[String]? {
return nil
}
}

这样所有需要被转换的类都需要继承Model类。但是使用的时候就比较尴尬了,必须要提供一个初始值,下面是我们的测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import UIKit
import INSSwift
class Person: NSObject {
var name: String = ""
var gender : Bool = false
var array: Array = [""]
var dictionary: Dictionary<String, Any> = ["": ""]
var student: Student = Student()
var teachers: [Teacher] = []
var ignored: String = ""
override class func keyMapping() ->Dictionary<String, String>? {
return [
"gender": "dictionary, gender",
"teachers": "student, t"
]
}
override class func classMapping() ->Dictionary<String, AnyClass>? {
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
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<String, String>? {
return nil
}
/// 提供的类对应Map
open class func classMapping() ->Dictionary<String, AnyClass>? {
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<String, String>?,
for object: AnyObject,
with clsMap: Dictionary<String, AnyClass>?,
with mapping: Dictionary<String, String>?,
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<String> = []
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<JSON> = JSONValue.arrayValue
var objectArray: Array<AnyObject> = []
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..<numericCast(count) {
let prop = properties?[Int(index)]
let pname = String(cString: property_getName(prop))
let ptype = String(cString: property_getAttributes(prop))
var propertyRealType: PropertyType
let index = ptype.index(ptype.startIndex, offsetBy: 2)
if let type = PropertyType.init(rawValue: ptype.substring(to: index)) {
propertyRealType = type
} else {
if ptype.hasPrefix(PropertyType.tString.rawValue) {
propertyRealType = PropertyType.tString
}
else if ptype.hasPrefix(PropertyType.tNSArray.rawValue) {
propertyRealType = PropertyType.tNSArray
}
else if ptype.hasPrefix(PropertyType.tNSDictionary.rawValue) {
propertyRealType = PropertyType.tNSDictionary
}
else if ptype.hasPrefix(PropertyType.tClass.rawValue) {
propertyRealType = PropertyType.tClass
} else {
propertyRealType = PropertyType.unknow
}
}
if !ignoredKeys.contains(pname) {
infoDict[pname] = propertyRealType.rawValue
}
}
free(properties)
guard infoDict.count != 0 else {
return nil
}
return infoDict
}
}
extension Dictionary {
mutating func merge<K, V>(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)
}
}
}

测试代码(测试的类在上边给出了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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,是不是很懒。

1
pod 'YYModel'

明确目标2:

  • 模型-数据库中的操作(数据库的基本语法在这里不做介绍)

说到模型-数据库中的操作,其实特别想把LKDBHelper拿来直接使用,这里比较懒,因为代码其实和上边差不多,多个数据库操作而已,所以不写了。LKDBHelper是一套OC的代码而且在Swift上测试没有问题。但是他的Demo是4个月前更新的,应该是Swift2.3版本,所以我们决定,fork一份代码并切转换到Swift3.0做测试,如果没问题,直接拿来用,以减少我们框架模块的编写时间。

好了,测试没有问题,并且提交了PR。我们现在拉过来使用,作者目前还没有合并到master,大家可以先来我的仓库查看使用的Demo。

1
pod 'LKDBHelper'

这样我们JSON-模型-数据库的代码基本完成了30%。剩下的都是后期的针对业务的修改。

网络请求模块

使用Alamofire,写到这里,读者肯定说我这个框架没有什么技术含量;要知道,这些库的成长经历了多少人的洗礼,而我只有3个人的小团队,创业公司的项目压力各位应该知道。所以合理的使用第三方库也是一个好的选择!如果你在一个大的团队,那么就可以为了晋升去写一点东西了。知道原理就好了,知道原理不写的原因是没有人家那么考虑的全面、构思的缜密。

新建文件Request.swift,Request对象作为在网络层流动的对象,Alamofire作为支撑。

1
pod 'Alamofire'

但是你真的以为我会这么做么?这么做太麻烦了,我很懒的。我们使用Moya+Alamofire。直接上使用流程:可以选择继承INSRequest来实现自己的Request类,也可以直接下一个枚举统一管理,但是Moya这里有个问题是:实现Moya的协议必须要实现所有的方法,该协议中所有的属性全部是required(而且让我不理解的是,sampleData这种非必须数据竟然不是,已经提了一个issue提问,因为文档没有很明确),对其中的一些属性做解释:

1
pod 'Moya'

记得把开发环境调整到iOS 9.0

baseURL: 根地址

path:网络请求路径

method:网络请求方法

parameters: 参数

sampleData:默认的Response data

task:当前任务的形态,一般为request,还有upload和download

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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的地址):

1
2
3
4
5
6
7
8
9
10
11
12
13
let provider = MoyaProvider<MMAPI>()
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顺便测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)")
}
1
2
3
4
5
⚒[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的时候,必须重写返回类名的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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%。

下一节开始写日志上报、异常捕获与处理、推送、位置、数据打点等。

评论