使用Combine来实现一个网络请求
通过上一篇《iOS响应式编程-Combine简介》的阅读,我们对Combine的大致应用有了一个基本的了解,接下来,我们看看Combine都能用在哪里。
首先是网络请求,因为网络请求有着天然的异步性,这一点完美的契合了Combine的特性-处理异步事件,所以,我们完全可以用Combine来写一个网络请求库,从而告别之前那种无穷无尽的回调方式。彻底告别callback,这是我们的学习目的。开始吧!!!
在编码之前,我们先列一下网络层的几个关键组成部分
- Endpoint:一个简单的struct,里面包含path和queryItem属性。通过对其进行扩展,我们可以很容易的创建一个base URL,来定义具体的endpoint和header。
- Codable模型:一个可以通过JSON进行编解码的模型
- 网络控制器:负责直接对接URLSessionDataTask,并且解码一个Codable模型的的数据结构。例如,具备一个get()方法用以返回一个包含某一模型的publisher
- 逻辑控制器:对来自View层的由网络控制器完成的业务进行抽象,并且提供简单的方法对模型进行序列化。例如,有一个getUser()方法,这个方法调用了一个网络控制器的必调方法,并且返回了一个users数组。
Codable 模型
接下来,我们将从API里获取用户列表,JSON格式如下:
{
"data": [
{
"id": "0F8JIqi4zwvb77FGz6Wt",
"title": "mr",
"firstName": "Heinz-Georg",
"lastName": "Fiedler",
"email": "heinz-georg.fiedler@example.com",
"picture": "https://randomuser.me/api/portraits/men/81.jpg"
},
{
"id": "0P6E1d4nr0L1ntW8cjGU",
"title": "miss",
"firstName": "Katie",
"lastName": "Hughes",
"email": "katie.hughes@example.com",
"picture": "https://randomuser.me/api/portraits/women/74.jpg"
}
]
}
所以,我们创建一个如下的模型:
struct Users: Codable, CustomStringConvertible {
let data: [User]?
}
struct User: Codable, CustomStringConvertible {
let id: String?
let title: String?
let firstName: String?
let lastName: String?
let email: String?
let picture: String?
}
需要注意的是这里我们同样遵循了CustomStringConvertible协议用以对我们的对象进行debug。我们用下面这个简单的扩展来减轻每个struct定义description属性的负担:
extension CustomStringConvertible where Self: Codable {
var description: String {
var description = "\n \(type(of: self)) \n"
let selfMirror = Mirror(reflecting: self)
for child in selfMirror.children {
if let propertyName = child.label {
description += "\(propertyName): \(child.value)\n"
}
}
return description
}
}
模型建立完成之后,开始其他部分的代码编写吧
EndPoint
定义一个Endpoint struct来匹配指定的API需求:
struct Endpoint {
var path: String
var queryItems: [URLQueryItem] = []
}
现在,我们需要创建一个扩展,在这个扩展里,我们对API的URL进行构建,而且也要定义包含APPID的headers属性(范例API特有的属性):
extension Endpoint {
var url: URL {
var components = URLComponents()
components.scheme = "https"
components.host = "dummyapi.io"
components.path = "/data/api" + path
components.queryItems = queryItems
guard let url = components.url else {
preconditionFailure("Invalid URL components: \(components)")
}
return url
}
var headers: [String: Any] {
return [
"app-id": "YOUR APP ID HERE"
]
}
}
定义一些Endpoints
extension Endpoint {
static var users: Self {
return Endpoint(path: "/user")
}
static func users(count: Int) -> Self {
return Endpoint(path: "/user",
queryItems: [
URLQueryItem(name: "limit",
value: "\(count)")
]
)
}
static func user(id: String) -> Self {
return Endpoint(path: "/user/\(id)")
}
}
我们只用第一个举例。后面两个将在后续用以展示便捷的添加参数和设置路径。
NetworkController
首先,定义一个协议:NetworkControllerProtocol
protocol NetworkControllerProtocol: class {
typealias Headers = [String: Any]
func get<T>(type: T.Type,
url: URL,
headers: Headers
) -> AnyPublisher<T, Error> where T: Decodable
}
可以看出,有一个方法来对单个Codable模型进行序列化
接下来,对这个协议进行具体实现
final class NetworkController: NetworkControllerProtocol {
func get<T: Decodable>(type: T.Type,
url: URL,
headers: Headers
) -> AnyPublisher<T, Error> {
var urlRequest = URLRequest(url: url)
headers.forEach { (key, value) in
if let value = value as? String {
urlRequest.setValue(value, forHTTPHeaderField: key)
}
}
return URLSession.shared.dataTaskPublisher(for: urlRequest)
.map(\.data)
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
对此总结以下四点:
- 创建并实现了一个URLSessionDataTask
- 如果没有错误发生,获取data
- 对模型对象进行Decode操作
- 返回一个要么包含一个模型对象,要么包含一个错误的publisher
LogicController
同样,也是实现一个协议:UsersLogicControllerProtocol
protocol UsersLogicControllerProtocol: class {
var networkController: NetworkControllerProtocol { get }
func getUsers() -> AnyPublisher<Users, Error>
func getUsers(count: Int) -> AnyPublisher<Users, Error>
func getUser(id: String) -> AnyPublisher<User, Error>
}
在这里,我们对NetworkController产生了依赖,并定义了三个方法:
- getUsers():默认对20个User进行序列化
- getUser(count: Int):指定一个具体数量的Users进行序列化
- getUser(id: String):对指定id的User进行序列化
final class UsersLogicController: UsersLogicControllerProtocol {
let networkController: NetworkControllerProtocol
init(networkController: NetworkControllerProtocol) {
self.networkController = networkController
}
func getUsers() -> AnyPublisher<Users, Error> {
let endpoint = Endpoint.users
return networkController.get(type: Users.self,
url: endpoint.url,
headers: endpoint.headers)
}
func getUsers(count: Int) -> AnyPublisher<Users, Error> {
let endpoint = Endpoint.users(count: count)
return networkController.get(type: Users.self,
url: endpoint.url,
headers: endpoint.headers)
}
func getUser(id: String) -> AnyPublisher<User, Error> {
let endpoint = Endpoint.user(id: id)
return networkController.get(type: User.self,
url: endpoint.url,
headers: endpoint.headers)
}
}
可以看到,我们的业务直接对接NetworkControlelr的方法。通过标识符final,用来表明,该类不可继承。 最后,来看看其他部分
如何使用
初始化一个NetworkController和UserLogicController
let networkController = NetworkController()
let usersLogicController = UsersLogicController(
networkController: networkController
)
创建一个suscriptions属性来存储以后的订阅:
let networkController = NetworkController()
let usersLogicController = UsersLogicController(
networkController: networkController
)
var subscriptions = Set<AnyCancellable>()
现在我们可以通过下面的方式来获取users
usersLogicController.getUsers()
.sink(receiveCompletion: { (completion) in
switch completion {
case let .failure(error):
print("Couldn't get users: \(error)")
case .finished: break
}
}) { users in
print(users)
}
.store(in: &subscriptions)
结果如下: