Swift面向协议和Codable的网络层设计

感慨

刚刚经历完毕业季,终于能有点空余时间来弄弄自己的博客了~
之前一直面试找工作,弄毕设(iOS端和后台都是用Swift写的 项目地址),写论文,和同学道别😭。。。

正文

前言

Codable是Swift4中新引入的一个协议。我一开始了解到这个协议是看了王巍的这篇博客不同角度看问题 - 从 Codable 到 Swift 元编程 当然,🐱神的这篇博客主要讲的是Mirror(映射对象的属性)

Codabel概念与使用

Codable是一个编码(Encoding)和解码(Decoding)的协议。类或者结构体遵循该协议,能让数据自动解码并生成该对象的实例,前提是该对象的属性都遵循Codable协议(常见有的String,Int,Double,Date,Data,URL),如果属性是Array或者Dictionary等集合类型,则必须保证集合类型中的元素都遵循Codable协议,像Dictionary< String, Any>类型中的Any就不遵循Codable协议,当然后面我会讲如何尽量在开发中避免该情况。

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
let jsonStr = """
{
"token": "fjawicnwgqybcgysd",
"student_number": 14104405,
"student_name": "Tanner Jin",
"age": 21,
"teachers": [
{
"name": "Wang",
"age": 39
},
{
"name": "Wu",
"age": 42
}
]
}
"""

struct Teacher: Codable {
var name: String
var age: Int
}

struct Student: Codable {

var token: String
var name: String
var age: Int
var teachers: Array<Teacher>

// 如果JSON中没有该字段,要将该属性设为Optional类型, 否则解析失败
var address: String?

enum CodingKeys: String, CodingKey {
case token
case age
case teachers
case address
case name = "student_name" // 用于JSON中字段和属性绑定
}
}

if let data = jsonStr.data(using: .utf8) {
do {
let student = try JSONDecoder().decode(Student.self, from: data)
print(student.teachers[0].name) // Wang
} catch {
print(error.localizedDescription)
}
}

Codabel扩展

Codabel也支持枚举类型,如下~

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
let jsonStr = """
{
"name": "Tanner",
"nation": "China",
"nation2": "Engish"
}
"""
// 枚举必须也遵循Codable协议,且有值(raw type)
enum CountryType: String, Codable {
case CN = "China"
case US = "United States"
case Engish
}

struct People: Codable {
var name: String
var nation: CountryType
var nation2: CountryType?
}

/// JSON字符串转模型
if let data = jsonStr3.data(using: .utf8) {
do {
let people = try JSONDecoder().decode(People.self, from: data)
print(people.nation) // CountryType.CN
print(people.nation2) // Optional(CountryType.Engish)
} catch {
print(error.localizedDescription)
}
}

网络层的设计

之前实习的时候接触过一个项目(当然原项目是Objective-C写的,我进入项目组后,强行弄成和Swift混编的项目,走过不少的坑,也坑了不少人😂)
原项目的的网络层主要是3层,最基本的一层是一个单例,基于AFN的AFHTTPSessionManager封装的一层网络请求(这一层是对项目无依赖的,单独拿出来放到哪个项目中都能用)。 第二层主要是基于项目的特性对Request和Response的设置统一设置,调用的是第一层的接口。第三层就是调用第二层的请求方法,然后每个接口写一个类方法(最后改类有500多行,还没加上分类的200多行的接口),然后在Controller中调用第三层的类方法实现业务。。。
由于之前没接触过其他大的项目,后面在自己的Swift项目中也借鉴了这种网络层的设计。
最近接触到另外一种更符合Swift的设计,就是今天的主题了~

1. 模型协议(CodableProtocol)

简单的封装了一下Codable协议,能够实现字典,JSONString,Data和模型的相互装换。
代码地址
(如果转化失败,可以查看抛出的错误)

2. 请求协议(RequestProtocol)

由于协议能定义关联对象,因此可以定义一个RequestProtol协议,关联一个遵循Codable协议的对象(作为模型)。至此,一个接口对应一个请求,一个请求对应一个模型

1
2
3
4
5
6
7
8
9
10
import Alamofire
protocol RequestProtocol {
associatedtype modelType: CodableProtocol // 模型
var url: String {get} // 接口
var params: Dictionary {get set} // 参数
var methon: HTTPHeaders {get} // 请求方法
var encoding: ParameterEncoding {get} // 数据编码

...(根据需求自己定义)
}

3. 配合封装网络请求方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 这里配合Alamofire以及SwiftyJSON使用
func requestData<T: RequestProtocol>(request: T, sucess: (_ model: T.modelType)->Void, failure: (_ error: Error)->Void)
{
Alamofire.request(request.url, method: request.methon, parameters: request.params, encoding: request.encoding, headers: request.header).validate().responseJSON { (result) in

switch result.result {
case .success(let value):
let jsonValue = JSON(value)
do {
// 该方法是CodableProtocol中封装好的方法
let model = try T.modelType.initialization(jsonValue)
sucess(model)
} catch {
failure(error)
}

case .failure(let error):
failure(error)

}
}

4. 具体使用例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

struct studentRequest: RequestProtocol {

// Student为上面列子的模型,当然遵循的是自定义的CodableProtocol协议
typealias modelType = Student

var url: String = "http://..."
var params: Dictionary<String, Any> = [:]
var methon: HTTPHeaders = .get
var encoding: ParameterEncoding = JSONEncoding.default

init(...) {...}
}

// 具体请求
let testRequest = studentRequest(...)

requestData(request: testRequest, sucess: { (model) in

print(model.teachers[0].name)
}, failure: { (error)
print(error)
})

5. 如何避免模型中的Any等不遵循Codable协议的类型

目前我的解决方法是在使用模型的这层(可能是Controller,也可能是ViewModel)中再建立一个中间层ModelHandle(数据处理加工层),即遵循CodableProtocol协议的模型是瘦模型,该模型的属性只存网络请求下来后的有用的数据。然后在ModelHandle中对模型进行处理加工,添加一些Controller或者ViewModel需要的但不遵循Codable协议的一些属性。然后在Controller或者ViewModel中使用数据加工层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct StudentModelHandle {

var model: Student
var viewData: Dictionary<String, Any>

// 将Student的address属性提出来(如果后台返回的JOSN一定没有该字段的情况下)
var address: String

init() {
handelModel()
}

private func handleModel() {
}
}