一、前言
最近进入新公司开展新项目,我发现公司项目的网络层很 OC ,最让人无法忍受的是数据解析是在网络层之外的,每一个数据模型都需要单独写解析代码。趁着项目才开始,给大家讲解Swift 运用协议泛型封装网络层
(其实做为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS学习交流群783941081,不管你是小白还是大牛欢迎入驻,大家一起交流学习)
二、Moya工具和Codable协议简介
这里只是展示一下 Moya 的基本使用方法和 Codable协议 的基本知识,如果对这两块感兴趣,读者可以自行去搜索研究。
- Moya工具
在OC中,我们使用AFNetworking来进行网络请求,简洁方便。在swift中,我们使用Moya来进行网络请求,Moya封装了Alamofire,可以更加方便的进行网络请求。初次使用Moya,还是觉得稍稍有些不习惯。在这里,记录下使用过程。
let url = URL(string: "your url")!
Alamofire.request(url).response { (response) in
// handle response
}
当然读者也会基于它进行二次封装,不会仅仅是上面代码那么简单。
如果使用 Moya, 你首先做的不是直接请求,而是根据项目模块建立一个个文件定义接口。例如我喜欢根据模块的功能取名 模块名 + API,然后再在其中定义我们需要使用的接口,例:
import Foundation
import Moya
enum YourModuleAPI {
case yourAPI1
case yourAPI2(parameter: String)
}
extension YourModuleAPI: TargetType {
var baseURL : URL {
return URL(string: "your base url")!
}
var headers : [String : String]? {
return "your header"
}
var path: String {
switch self {
case .yourAPI1:
return "yourAPI1 path"
case .yourAPI2:
return "yourAPI2 path"
}
}
var method: Moya.Method {
switch self {
case .yourAPI1:
return .post
default:
return .get
}
}
// 这里只是带参数的网络请求
var task: Task {
var parameters: [String: Any] = [:]
switch self {
case let .yourAPI1:
parameters = [:]
case let .yourAPI2(parameter):
parameters = ["字段":parameter]
}
return .requestParameters(parameters: parameters,
encoding: URLEncoding.default)
}
// 单元测试使用
var sampleData : Data {
return Data()
}
}
定义如上的文件后,你就可以使用如下方式进行网络请求:
MoyaProvider().request(YourModuleAPI.yourAPI1) { (result) in
// handle result
}
- Codable协议
自Swift4发布以来已有一段时间了,各种新特性为我们提供更加高效的开发效率,其中在Swift4中使用Codable协议进行模型与json数据之间的映射提供更加便利的方式。在Swift3中,对于从服务器获取到的json数据后,我们要进行一系列繁琐的操作才能将数据完整的转化成模型,举个,我们从服务器获取了一段这样的json数据:
{
"student": {
"name": "Jone",
"age": 18,
"finger": {
"count": 10
}
}
}
然后我们用JSONSerialization来解析数据,得到的是一个Any类型。当我们要读取count时就要采取以下操作:
let json = try! JSONSerialization.jsonObject(with: data, options: [])
if let jsonDic = json as? [String:Any] {
if let student = jsonDic["student"] as? [String:Any] {
if let finger = student["finger"] as? [String:Int] {
if let count = finger["count"] {
print(count)
}
}
}
}
在日常用Swift编写代码时,就我而言,我喜欢使用SwiftyJSON或则ObjectMapper来进行json转模型,因为相比原生的,使用这些第三方会给我们带来更高的效率。于是在Swift4中,Apple官方就此提供了自己的方法,现在我们来了解其基本的用法。
三、Codable的简单使用
首先,我们来对最简单的json数据进行转模型,现在我们有以下一组json数据:
let res = """
{
"name": "Jone",
"age": 17
}
"""
let data = res.data(using: .utf8)!
然后我们定义一个Student结构体作为数据的模型,并遵守Codable协议:
struct Student: Codable {
let name: String
let age: Int
}
而关于Codable协议的描述我们可以点进去看一下:
public typealias Codable = Decodable & Encodable
public protocol Encodable {
public func encode(to encoder: Encoder) throws
}
public protocol Decodable {
public init(from decoder: Decoder) throws
}
其实就是遵守一个关于解码的协议和一个关于编码的协议,只要遵守这些协议才能进行json与模型之间的编码与解码。
接下来我们就可以进行讲json解码并映射成模型:
let decoder = JSONDecoder()
let stu = try! decoder.decode(Student.self, from: data)
print(stu) //Student(name: "Jone", age: 17)
然后,我们可以将刚才得到的模型进行编码成json:
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted //输出格式好看点
let data = try! encoder.encode(stu)
print(String(data: data, encoding: .utf8)!)
//{
// "name" : "Jone",
// "age" : 17
//}
就是这么简单~~~
这里对encode和decode使用try!是为了减少文章篇幅,正常使用时要对错误进行处理,而常见的错误会在第三篇讲到
四、分析和解决方案
4.1.1重复解析数据到模型
例如这里有两个接口,一个是请求商品列表,一个是请求商城首页。笔者以前是这样写的:
enum MallAPI {
case getMallHome
case getGoodsList
}
extension MallAPI: TargetType {
// 略
}
let mallProvider = MoyaProvider()
mallProvider.request(MallAPI.getGoodsList) { (response) in
// 将 response 解析成 Goods 模型数组用 success 闭包传出去
}
mallProvider.request(MallAPI.getMallHome) { (response) in
// 将 response 解析成 Home 模型用 success 闭包传出去
}
以上是简化的实用场景,每一个网络请求都会单独的写一次将返回的数据解析成数据模型或者数据模型数组。就算是将数据解析的功能封装成一个单例工具类,也仅仅是稍稍好了一些。
笔者想要的是指定数据模型类型后,网络层直接返回解析完成后的数据模型供我们使用。
41..2 运用泛型来解决
泛型就是用来解决上面这种问题的,
使用泛型创建一个网络工具类,并给定泛型的条件约束:遵守 Codable 协议。
struct NetworkManager where T: Codable {
}
这样我们在使用时,就可以指定需要解析的数据模型类型了。
NetworkManager().reqest...
NetworkManager().reqest...
细心的读者会发现这和 Moya 初始化 MoyaProvider 类的使用方式一样。
4.1.3使用Moya后,如何将加载控制器和缓存封装到网络层
由于使用了 Moya 进行再次封装,每对代码进行一次封装的代价就是自由度的牺牲。如何将加载控制器&缓存功能和 Moya 契合起来呢?
一个很简单的做法是在请求方法里添加是否显示控制器和是否缓存布尔值参数。看着我的请求方法参数已经5,6个,这个方案立马被排除了。看着 Moya 的 TargetType 协议,给了我灵感。
4.2.1 运用协议来解决
既然 MallAPI 能遵守 TargetType 来实现配置网络请求信息,那当然也能遵守我们自己的协议来进行一些配置。
自定义一个 Moya 的补充协议
protocol MoyaAddable {
var cacheKey: String? { get }
var isShowHud: Bool { get }
}
这样 MallAPI 就需要遵守两个协议了
extension MallAPI: TargetType, MoyaAddable {
// 略
}
五、部分代码展示和解析
完整的代码,读者可以到 Github 上去下载。
5.1 封装后的网络请求
通过给定需要返回的数据类型,返回的 response 可以直接调取 dataList 属性获取解析后的 Goods 数据模型数组。错误闭包里面也能直接通过 error.message 获取报错信息,然后根据业务需求选择是否使用弹出框提示用户。
NetworkManager().requestListModel(MallAPI.getOrderList,
completion: { (response) in
let list = response?.dataList
let page = response?.page
}) { (error) in
if let msg = error.message else {
print(msg)
}
}
5.2 返回数据的封装
笔者公司服务端返回的数据结构大致如下:
{
"code": 0,
"msg": "成功",
"data": {
"hasMore": false,
"list": []
}
}
出于目前业务和解析数据的考虑,笔者将返回的数据类型封装成了两类,同时也将解析的操作放在了里面。
后面的请求方法也分成了两个,这不是必要的,读者可以根据自己的业务和喜好选择。
请求列表接口返回的数据
请求普通接口返回的数据
class BaseResponse {
var code: Int { ... } // 解析
var message: String? { ... } // 解析
var jsonData: Any? { ... } // 解析
let json: [String : Any]
init?(data: Any) {
guard let temp = data as? [String : Any] else {
return nil
}
self.json = temp
}
func json2Data(_ object: Any) -> Data? {
return try? JSONSerialization.data(
withJSONObject: object,
options: [])
}
}
class ListResponse: BaseResponse where T: Codable {
var dataList: [T]? { ... } // 解析
var page: PageModel? { ... } // 解析
}
class ModelResponse: BaseResponse where T: Codable {
var data: T? { ... } // 解析
}
这样我们直接返回相应的封装类对象就能获取解析后的数据了。
5.3 错误的封装
网络请求过程中,肯定有各种各样的错误,这里使用了 Swift 语言的错误机制。
// 网络错误处理枚举
public enum NetworkError: Error {
// 略...
// 服务器返回的错误
case serverResponse(message: String?, code: Int)
}
extension NetworkError {
var message: String? {
switch self {
case let .serverResponse(msg, _): return msg
default: return nil
}
}
var code: Int {
switch self {
case let .serverResponse(_, code): return code
default: return -1
}
}
}
这里的扩展很重要,它能帮我们在处理错误时获取错误的 message 和 code.
5.4 请求网络方法
最终请求的方法
private func request(
_ type: R,
test: Bool = false,
progressBlock: ((Double) -> ())? = nil,
modelCompletion: ((ModelResponse?) -> ())? = nil,
modelListCompletion: ((ListResponse?) -> () )? = nil,
error: @escaping (NetworkError) -> () )
-> Cancellable?
{}
这里的 R 泛型是用来获取 Moya 定义的接口,指定了必须同时遵守 TargetType 和 MoyaAddable 协议,其余的都是常规操作了。
和封装的返回数据一样,这里也分了普通接口和列表接口。
@discardableResult
func requestModel(
_ type: R,
test: Bool = false,
progressBlock: ((Double) -> ())? = nil,
completion: @escaping ((ModelResponse?) -> ()),
error: @escaping (NetworkError) -> () )
-> Cancellable?
{
return request(type,
test: test,
progressBlock: progressBlock,
modelCompletion: completion,
error: error)
}
@discardableResult
func requestListModel(
_ type: R,
test: Bool = false,
completion: @escaping ((ListResponse?) -> ()),
error: @escaping (NetworkError) -> () )
-> Cancellable?
{
return request(type,
test: test,
modelListCompletion: completion,
error: error)
}
我综合目前项目和 Codable 协议的坑点考虑,将这里写得有点死板,万一来个既是列表又有其他数据的就不适用了。不过到时候可以添加一个类似这种方法,将数据传出去处理。
// Demo里没有这个方法
func requestCustom(
_ type: R,
test: Bool = false,
completion: (Response) -> ()) -> Cancellable?
{
// 略
}
5.5 缓存和加载控制器
想到添加 MoyaAddable 协议后,其他就没什么困难的了,直接根据 type 获取接口定义文件中的配置做出相应的操作就行了。
var cacheKey: String? {
switch self {
case .getGoodsList:
return "cache goods key"
default:
return nil
}
}
var isShowHud: Bool {
switch self {
case .getGoodsList:
return true
default:
return false
}
}
这就添加了 getGoodsList 接口请求中的两个功能
请求返回数据后会通过给定的缓存 Key 进行缓存
网络请求过程中自动显示和隐藏加载控制器。
如果读者的加载控制器有不同的样式,还可以添加一个加载控制器样式的属性。甚至缓存的方式是同步还是异步,都可以通过这个 MoyaAddable 添加。
// 缓存
private func cacheData(
_ type: R,
modelCompletion: ((Response?) -> ())? = nil,
modelListCompletion: ( (ListResponse?) -> () )? = nil,
model: (Response?, ListResponse?))
{
guard let cacheKey = type.cacheKey else {
return
}
if modelComletion != nil, let temp = model.0 {
// 缓存
}
if modelListComletion != nil, let temp = model.1 {
// 缓存
}
}
加载控制器的显示和隐藏使用的是 Moya 自带的插件工具。
// 创建moya请求类
private func createProvider(
type: T,
test: Bool)
-> MoyaProvider
{
let activityPlugin = NetworkActivityPlugin { (state, targetType) in
switch state {
case .began:
DispatchQueue.main.async {
if type.isShowHud {
SVProgressHUD.showLoading()
}
self.startStatusNetworkActivity()
}
case .ended:
DispatchQueue.main.async {
if type.isShowHud {
SVProgressHUD.dismiss()
}
self.stopStatusNetworkActivity()
}
}
}
let provider = MoyaProvider(
plugins: [activityPlugin,
NetworkLoggerPlugin(verbose: false)])
return provider
}
5.6 避免重复请求
定义一个数组来保存网络请求的信息,一个并行队列使用 barrier 函数来保证数组元素添加和移除线程安全。
// 用来处理只请求一次的栅栏队列
private let barrierQueue = DispatchQueue(label: "cn.tsingho.qingyun.NetworkManager",
attributes: .concurrent)
// 用来处理只请求一次的数组,保存请求的信息 唯一
private var fetchRequestKeys = [String]()
private func isSameRequest(_ type: R) -> Bool {
switch type.task {
case let .requestParameters(parameters, _):
let key = type.path + parameters.description
var result: Bool!
barrierQueue.sync(flags: .barrier) {
result = fetchRequestKeys.contains(key)
if !result {
fetchRequestKeys.append(key)
}
}
return result
default:
// 不会调用
return false
}
}
private func cleanRequest(_ type: R) {
switch type.task {
case let .requestParameters(parameters, _):
let key = type.path + parameters.description
barrierQueue.sync(flags: .barrier) {
fetchRequestKeys.remove(key)
}
default:
// 不会调用
()
}
}
这种实现方式目前有一个小问题,多个界面使用同一接口,并且参数也相同的话,只会请求一次,不过这种情况还是极少的,暂时没遇到就没有处理。
六、后记
目前封装的这个网络层代码有点强业务类型,毕竟我的初衷就是给自己公司项目重新写一个网络层,因此可能不适用于某些情况。不过这里使用泛型和协议的方法是通用的,读者可以使用同样的方式实现匹配自己项目的网络层。如果读者有更好的建议,还希望评论出来一起讨论。