使用Combine来实现一个网络请求

6,110 阅读4分钟

通过上一篇《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)

结果如下:

image.png