前言
Combine是苹果2019WWDC推出的响应式编程框架,而在此之前,我们一般使用RxSwift来实现。在过往的项目中,我们会通过RxSwift在网络请求中监听回调,在MVVM中实现View与ViewModel的双向绑定,或是用来监听某些事件如WebSocket的消息、用户登录登出、数据库更新等等。
本文会将上述在UIKit使用RxSwift的场景分别用Combine进行替换。如果你想先了解Combine的相关概念,我相信你能轻松地找到相应的文章,这里推荐一篇从响应式编程到 Combine 实践。
网络请求
URLSession
苹果给URLSession增加了支持Combine的扩展方法URLSession.shared.dataTaskPublisher,来做一次简单的网络请求:
import Foundation
import Combine
class NetworkPublisherTest {
static let shared = NetworkPublisherTest()
private var cancellables = Set<AnyCancellable>()
func request() {
URLSession.shared.dataTaskPublisher(for: <#T##URL#>)
.receive(on: DispatchQueue.main)
.tryMap({ try JSONDecoder().decode(DataModel.self, from: $0.data) })
.sink { completion in
guard case let .failure(error) = completion else { return }
print(error)
} receiveValue: { model in
print(model)
}.store(in: &cancellables)
}
}
struct DataModel: Codable {
let code: Int?
let data: UserModel?
let msg: String?
}
struct UserModel: Codable {
let userId: String?
let userName: String?
let avatarUrl: String?
}
首先用dataTaskPublisher发起请求,通过receive在主线程接收响应,利用tryMap将响应的数据反序列化,至于DataModel则根据你的api接口返回结构设计,然后sink处理响应回调,我们在receiveValue:闭包内就可以得到我们想要的数据模型了。最后记得store一下,否则Cancellable对象被直接释放,将无法接收到回调。
Moya
如果你像我一样正在使用Moya,你可以直接安装Moya/Combine,它在Moya的基础上提供了一层Combine的扩展,了解这层扩展可以帮助我们更好地理解Combine,下面看一下Moya/Combine是如何实现这层扩展的。
MoyaProvider+Combine
首先看API的其中一个入口,初始化了一个MoyaPublisher并在闭包内调用了Moya/Core层的网络请求接口,当收到响应时,根据不同的响应结果,让subscriber对象用不同方法接收。
func requestPublisher(_ target: Target, callbackQueue: DispatchQueue? = nil) -> AnyPublisher<Response, MoyaError> {
return MoyaPublisher { [weak self] subscriber in
return self?.request(target, callbackQueue: callbackQueue, progress: nil) { result in
switch result {
case let .success(response):
_ = subscriber.receive(response)
subscriber.receive(completion: .finished)
case let .failure(error):
subscriber.receive(completion: .failure(error))
}
}
}
.eraseToAnyPublisher()
}
MoyaPublisher
下面我们看看MoyaPublisher做了什么,我们可以看到,MoyaPublisher初始化后将callback闭包缓存了起来:
init(callback: @escaping (AnySubscriber<Output, MoyaError>) -> Moya.Cancellable?) {
self.callback = callback
}
然后在receive<S>(subscriber: S)里面传给了Subscription,这是一个Publisher协议方法,在subscribe(_ :)里去调用,也就是发布者被订阅的时候触发:
internal func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = Subscription(subscriber: AnySubscriber(subscriber), callback: callback)
subscriber.receive(subscription: subscription)
}
接下来我们去看一下Moya定义的Subscription类,Subscription实现了Combine.Subscription协议的两个方法,request(_:)和cancel(),在request(_:)中判断是否有需求来减轻背压,然后执行网络请求,并将请求返回的Cancellable缓存起来,供需要取消请求的时候使用:
private class Subscription: Combine.Subscription {
private let performCall: () -> Moya.Cancellable?
private var cancellable: Moya.Cancellable?
init(subscriber: AnySubscriber<Output, MoyaError>, callback: @escaping (AnySubscriber<Output, MoyaError>) -> Moya.Cancellable?) {
performCall = { callback(subscriber) }
}
func request(_ demand: Subscribers.Demand) {
guard demand > .none else { return }
cancellable = performCall()
}
func cancel() {
cancellable?.cancel()
}
}
小结
总的来说就是,通过实现Publisher协议的receive<S>(subscriber: S),将网络请求这一动作放到接收到观察者之后,并且通过一个实例实现Combine.Subscription协议,管理网络请求的生命周期。
MVVM双向绑定
关于双向绑定,这里主要讨论Combine在UIKit的使用,有CurrentValueSubject(不推荐)以及@Published。
CurrentValueSubject
ViewModel:
import Foundation
import Combine
class ViewModel {
private(set) var title = CurrentValueSubject<String?, Never>(nil)
private(set) var dataSource = CurrentValueSubject<[String], Never>([])
func requestData() {
// 请求数据...
dataSource.send(data) // 接收到数据回调
}
}
ViewController:
import UIKit
import Combine
class ViewController {
private let collectionView = UICollectionView()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// 绑定
viewModel.title
.assign(to: \.title, on: navigationItem)
.store(in: &cancellables)
viewModel.dataSource.sink { [weak self] data in
self?.collectionView.reloadData()
}.store(in: &cancellables)
// 发起数据网络请求
viewModel.requestData()
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel.dataSource.value.count
}
}
@Published
ViewModel:
import Foundation
import Combine
class ViewModel {
@Published private(set) var title: String? = ""
@Published private(set) var dataSource = [String][]
func requestData() {
// 请求数据...
dataSource = data// 接收到数据回调
}
}
ViewController:
import UIKit
import Combine
class ViewController {
private let collectionView = UICollectionView()
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// 绑定
viewModel.$title
.assign(to: \.title, on: navigationItem)
.store(in: &cancellables)
viewModel.$dataSource.sink { [weak self] data in
self?.collectionView.reloadData()
}.store(in: &cancellables)
// 发起数据网络请求
viewModel.requestData()
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel.dataSource.count
}
}
小结
对比之下可以看到,虽然两种写法都能达到我们的期望,但是使用@Published对代码的侵入更小,而且在SwiftUI中我们同样会使用@Published去修饰需要监听的参数。
监听事件
PassthroughSubject
透传事件,与CurrentValueSubject的区别在于不会持有最新的 Output,也没有初始值,一般会在单例中使用,将数据传递给外部的多个对象,跟Notification的作用相似,好处就是比Notification更加直观,可维护性也会稍强。用法上跟CurrentValueSubject相似,具体可以看上文,不再赘述。
结尾
Combine与RxSwfit总体上十分相似,在项目整个替换的过程不会耗费太多effort,并且有着一方库、减少包体积、性能提升的优势,将RxSwift替换成Combine是一件性价比很高的事🚀