博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Swift 运用协议泛型封装网络层
阅读量:7021 次
发布时间:2019-06-28

本文共 10674 字,大约阅读时间需要 35 分钟。

swift 版本: 4.1

Xcode 版本 9.3 (9E145)

基于 和 再封装

代码 Github 地址:

一、前言

最近进入新公司开展新项目,我发现公司项目的网络层很 OC ,最让人无法忍受的是数据解析是在网络层之外的,每一个数据模型都需要单独写解析代码。趁着项目才开始,我提议由我写一个网络层小工具来代替以前的网络层,顺便把加载菊花,缓存也封装到了里面。

二、Moya工具和Codable协议简介

这里只是展示一下 Moya 的基本使用方法和 Codable协议 的基本知识,如果对这两块感兴趣,读者可以自行去搜索研究。

2.1 Moya工具

使用 Moya 是因为笔者觉得它很方便,如果读者不想使用 Moya,也不影响你阅读这篇文章的内容。

Alamofire 这里就不作介绍了,如果没有接触过,你可以把它当做是 Swift 版本的 AFNetworkingMoya 是一个对 Alamofire 进行了再次封装的工具库。如果只使用 Alamofire ,你的网络请求可能会是这样:

let url = URL(string: "your url")!Alamofire.request(url).response { (response) in    // handle response}复制代码

当然读者也会基于它进行二次封装,不会仅仅是上面代码那么简单。

如果使用 Moya, 你首先做的不是直接请求,而是根据项目模块建立一个个文件定义接口。例如我喜欢根据模块的功能取名 模块名 + API,然后再在其中定义我们需要使用的接口,例:

import Foundationimport Moyaenum 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 }复制代码

2.2 Codable协议

Codable协议Swift4 才更新的,用来解析和编码数据,它是由编码协议和解码协议组成。

public typealias Codable = Decodable & Encodable复制代码

Swift 更新 Codable协议 之前,笔者一直用的 来解析网络请求返回的数据。最近使用 Codable协议 后,发现还蛮好用的,就直接用上了。

不过 Codable协议 还是有一些坑点的,例如这篇文章所描述的:

下面的 Person 模型类储存了一个简单的个人信息,这里只是使用了解码,所以只遵守了 Decodable协议

struct Person: Decodable {  var name: String  var age: Int}复制代码

StringInt 是系统默认的可编解码类型,所以我们无需再写其他代码了,编译器将默认为我们实现。

let jsonString = """        {   "name": "swordjoy",            "age": 99        }"""if let data = jsonString.data(using: .utf8) {    let decoder = JSONDecoder()    if let person = try? decoder.decode(Person.self, from: data) {        print(person.age)    // 99        print(person.name)   // swordjoy    }}复制代码

只需要将 Person 类型传给 JSONDecoder 对象,它就能直接将 JSON 数据转换成 Person 数据模型对象。实际使用中由于解析规则的各种严格的限制,远远没有上面看着这么简单。

三、分析和解决方案

3.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 闭包传出去}复制代码

以上是简化的实用场景,每一个网络请求都会单独的写一次将返回的数据解析成数据模型或者数据模型数组。就算是将数据解析的功能封装成一个单例工具类,也仅仅是稍稍好了一些。

笔者想要的是指定数据模型类型后,网络层直接返回解析完成后的数据模型供我们使用。

3.1.2 运用泛型来解决

泛型就是用来解决上面这种问题的, 使用泛型创建一个网络工具类,并给定泛型的条件约束:遵守 Codable 协议。

struct NetworkManager
where T: Codable { }复制代码

这样我们在使用时,就可以指定需要解析的数据模型类型了。

NetworkManager
().reqest...NetworkManager
().reqest...复制代码

细心的读者会发现这和 Moya 初始化 MoyaProvider 类的使用方式一样。

3.2.1 使用Moya后,如何将加载控制器和缓存封装到网络层

由于使用了 Moya 进行再次封装,每对代码进行一次封装的代价就是自由度的牺牲。如何将加载控制器&缓存功能和 Moya 契合起来呢?

一个很简单的做法是在请求方法里添加是否显示控制器和是否缓存布尔值参数。看着我的请求方法参数已经5,6个,这个方案立马被排除了。看着 MoyaTargetType 协议,给了我灵感。

3.2.2 运用协议来解决

既然 MallAPI 能遵守 TargetType 来实现配置网络请求信息,那当然也能遵守我们自己的协议来进行一些配置。

自定义一个 Moya 的补充协议

protocol MoyaAddable {    var cacheKey: String? { get }    var isShowHud: Bool { get }}复制代码

这样 MallAPI 就需要遵守两个协议了

extension MallAPI: TargetType, MoyaAddable {    // 略   }复制代码

四、部分代码展示和解析

完整的代码,读者可以到 Github 上去下载。

4.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) }}复制代码

4.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? { ... } // 解析}复制代码

这样我们直接返回相应的封装类对象就能获取解析后的数据了。

4.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        }    }}复制代码

这里的扩展很重要,它能帮我们在处理错误时获取错误的 messagecode.

4.4 请求网络方法

最终请求的方法

private func request
( _ type: R, test: Bool = false, progressBlock: ((Double) -> ())? = nil, modelCompletion: ((ModelResponse
?) -> ())? = nil, modelListCompletion: ((ListResponse
?) -> () )? = nil, error: @escaping (NetworkError) -> () ) -> Cancellable?{}复制代码

这里的 R 泛型是用来获取 Moya 定义的接口,指定了必须同时遵守 TargetTypeMoyaAddable 协议,其余的都是常规操作了。 和封装的返回数据一样,这里也分了普通接口和列表接口。

@discardableResultfunc 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)}@discardableResultfunc 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? { // 略}复制代码

4.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}复制代码

4.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: // 不会调用 () }}复制代码

这种实现方式目前有一个小问题,多个界面使用同一接口,并且参数也相同的话,只会请求一次,不过这种情况还是极少的,暂时没遇到就没有处理。

五、后记

目前封装的这个网络层代码有点强业务类型,毕竟我的初衷就是给自己公司项目重新写一个网络层,因此可能不适用于某些情况。不过这里使用泛型和协议的方法是通用的,读者可以使用同样的方式实现匹配自己项目的网络层。如果读者有更好的建议,还希望评论出来一起讨论。

转载评论留转载地址即可转载。

你可能感兴趣的文章
TSMessages,非HUD风格的iOS提示框(附官方demo BUG修复方案)
查看>>
我的友情链接
查看>>
Linux汇编GAS调用C语言函数实例
查看>>
linux-samba服务配置
查看>>
源码安装mysql5.6.20&&mysql主从设置(多实例做多个主从)
查看>>
ProgressDialog 简单使用记录
查看>>
docker centos7 容器上安装ssh
查看>>
DOS下远程共享磁盘
查看>>
从零开始的Spring Security Oauth2(一)
查看>>
Tomcat启动分析
查看>>
C++学习笔记(5):指针函数与函数指针
查看>>
Android 搭载Linux内核:三星放出源代码
查看>>
收藏链接地址,设为主页
查看>>
union union al l
查看>>
day-19:linux下打包tar工具及ZIP介绍
查看>>
lamp优化
查看>>
Bitshare2.0
查看>>
js实现继承方式
查看>>
单例模式 工厂模式
查看>>
使用Ext.grid.column.Column定义列
查看>>