RxSwift to Combine

14,575 阅读2分钟

前言

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是一件性价比很高的事🚀