Combine对XTDemo中VM的改造

0 2.JPEG

# 前言

上篇博文中,已经将 RxSwift 替换为了 Combine,开头也提到了只是简单的使用 Combine。此篇中作为这两天学习的一个小结。通过对 ViewModel 的改造来进一步学习 Combine 的使用。

  • 文中不会过多介绍 CombinePublisers, Subscribers 的类型——官方注释很详细并且上篇中也有相关博客/文章链接。

  • 文中以 XTDemoTextureDemoViewController 的改造为例进行说明。

  • 效果图:

阅读本文您将得到:

  • 使用 Publisher 封装 MJRefresh 刷新事件(UIContol 与此类似)。

  • 使用 Subscriber 封装 MJRefresh 结束刷新的方法

  • ViewModelInput 中方法调用改为订阅者(AnySubscriber<Input, Never> )属性

  • 通过操作符创建新的 Publisher

本文中将涉及的方法主要有:

  • Publisher

    func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
    复制代码
  • AnySubscriber

    @inlinable public init<S>(_ s: S) where Input == S.Input, Failure == S.Failure, S : Subscriber
    
    public init<S>(_ s: S) where Input == S.Output, Failure == S.Failure, S : Subject
    复制代码

# Publiser 和 Publishers, Subscriber 和 SubScribers

官方注释上我们能很明白的知道:

  • Publishers发布者(Publisher) 的命名空间。
  • 所有 发布者(Publisher) 的操作符(func)的实现都是通过 Publishers 内部定义类型实现的。
/// A namespace for types that serve as publishers.
///
/// The various operators defined as extensions on ``Publisher`` implement 
/// their functionality as classes or structures 
/// that extend this enumeration. For example, 
/// the `contains(_:)` operator returns a `Publishers.Contains` instance.
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public enum Publishers {
}
复制代码

同样的 Subscribers 也是 用作订阅者的类型的名称空间AnyCancellable 是对 Cancellable 的类型摸除。

Subscribersextension 中官方提供了 Subscribers.SinkSubscribers.Assign 等。 Subscribers.SinkPublisherfunc sink(xx) 对应,Subscribers.Sink 遵守 Cancellable 协议。 Subscribers.Assign 与之类似。

# 使用 AnySubscriber 定义 ViewModel.Input

ViewModel 的定义和 RxSwift补充 中所阐述的一样,这里需要将 Input 协议从 func xx 转变为订阅者 属性。使用 AnySubscriber 不限制内部具体使用类型,同时隐藏内部实现。

protocol TextureDemoViewModelInputs {

    var viewDidLoadSubscriber: AnySubscriber<Void, Never> { get }
    var refreshSubscriber: AnySubscriber<Void, Never> { get }
    var moreDataSubcriber: AnySubscriber<Void, Never> { get }
}
复制代码

例如:viewDidLoadSubscriber 对应原来的 func viewDidload()。 在 ViewModel 对其实现有两种方式:

  • 直接使用 Subject 转换:

    fileprivate let moreDataSubject = PassthroughSubject<Void, Never>()
    var moreDataSubcriber: AnySubscriber<Void, Never> {
        self.moreDataSubject.asAnySubscriber()
    }
    复制代码
  • 使用使用 Subscribers.xxx 创建:

    var viewDidLoadSubscriber: AnySubscriber<Void, Never> {
        let sinkSubscriber = Subscribers.Sink<Void, Never>.init { _ in
            print("viewDidLoad Sink finished! ____&")
        } receiveValue: { [weak self] _ in
            self?.queryNewData()
        }
    
        return .init(sinkSubscriber)
    }
    复制代码

# 为 MJRefresh 添加 Combine 支持

通过对 Input 的改造,我们很容易通过 Subscribers.Sink 实现 MJRefreshHeaderMJRefreshFooter 结束刷新的 subscriber

extension MJRefreshHeader {

    // @NOTE: - 可以是 方法, 也可以是 计算属性, 都不支持多次加入到 发布者 中
    func subscriber() -> AnySubscriber<Void, Never> {
        let sinkSubscriber = Subscribers.Sink<Void, Never>.init { _ in
            // To be continue
        } receiveValue: { [weak self] _ in
            self?.endRefreshing()
        }
        return .init(sinkSubscriber)
    }
}

extension MJRefreshFooter {

    func subscriber() -> AnySubscriber<Bool, Never> {
        let sinkSubscriber = Subscribers.Sink<Bool, Never>.init { _ in
            // To be continue
        } receiveValue: { [weak self] hasMore in
            (hasMore ? { self?.endRefreshing() } : { self?.endRefreshingWithNoMoreData() })()
        }
        return .init(sinkSubscriber)
    }
}
复制代码

对上/下拉刷新提供 Publisher 封装,需要用到自定义 PublisherSubscription 来组合实现。

本文想要将发送信息的 MJRefreshComponent 返回(也返回 Void),Subscription 定义如下:

fileprivate final class MJRefreshingSubscription<S: Subscriber, Control: MJRefreshComponent>: Subscription where S.Input == Control {

    private var subscriber: S
    private let control: Control

    init(subscriber: S, control: Control) {
        self.subscriber = subscriber
        self.control = control
        //control.setRefreshingTarget(self, refreshingAction: #selector(refreshing))
        // FIXDE: - 注意循环引用: control -> refreshingBlock -> control
        control.refreshingBlock = { [weak control] in
            if let ctr = control {
                _ = subscriber.receive(ctr)
            }
        }
    }

    // To be continue

    func request(_ demand: Subscribers.Demand) {
        guard demand > 0 else { return subscriber.receive(completion: .finished) }
        // 不作任何处理, 已经在 refreshingBlock 中通知 subscriber 接收 control
    }

    func cancel() {
        // To be continue
        subscriber.receive(completion: .finished)
    }
}
复制代码

Publisher 定义如下:

fileprivate final class MJRefreshingPublisher<Control: MJRefreshComponent>: Publisher {
    typealias Output = Control
    typealias Failure = Never

    let control: Control

    init(control: Control) {
        self.control = control
    }

    // To be continue

    func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Control == S.Input {
        let subscription = MJRefreshingSubscription(subscriber: subscriber, control: control)
        subscriber.receive(subscription: subscription)
    }
}
复制代码

MJRefreshComponent 的刷新提供 Publisher 支持:

extension MJRefreshComponent {

    var publisherRefreshing: AnyPublisher<MJRefreshComponent, Never> {
        return MJRefreshingPublisher(control: self).eraseToAnyPublisher()
    }
}

复制代码

# 通过操作符创建新的 Publisher

Outputs 协议定义如下:

protocol TextureDemoViewModelOutputs {

    var newDataPublisher: AnyPublisher<[DynamicDisplayType], Never> { get }
    var endRefreshPublisher: AnyPublisher<Void, Never> { get }
    var moreDataPublisher: AnyPublisher<[DynamicDisplayType], Never> { get }
    var endMoreRefreshPublisher: AnyPublisher<Bool, Never> { get }
    var toastPublisher: AnyPublisher<String, Never> { get }
}
复制代码

ViewModel 中我们任然需要通过 PassthroughSubject(或者 CurrentValueSubject) 来控制数据请求的时机(send(value), 或者 asAnySubscriber())。使用的 Subject 定义如下:

fileprivate let refreshSubject = PassthroughSubject<Void, Never>()
fileprivate let topicSubject = PassthroughSubject<Void, Never>()
fileprivate let moreDataSubject = PassthroughSubject<Void, Never>()
复制代码

OutputsPublisher 都是通过他们转换而来的。注意:需要使用 share() 操作符,来达到 Publisher 共享的作用(简单理解为引用类型和值类型的区别),下面是需要共享的 publisher

// FIXED: - 必须使用存储属性, share() 才能保证多次订阅不会产生多次的请求
private lazy var newDataResultPublisher: AnyPublisher<Result<[DynamicDisplayType], BundleJsonDataError>, Never> = {
    self.createNewDataPublisher()
        .share()
        .eraseToAnyPublisher()
}()

private lazy var moreDataResultPublisher: AnyPublisher<[DynamicDisplayType]?, Never> = {
    self.createMoreDataPublisher()
        .share()
        .eraseToAnyPublisher()
}()
复制代码

Outputs 中其他发布者转换入下:

var newDataPublisher: AnyPublisher<[DynamicDisplayType], Never> {
    return self.newDataResultPublisher
        .compactMap { result -> [DynamicDisplayType]? in
            if case .success(let list) = result {
                return list
            }
            return nil
        }
        .onMainScheduler()
}

var endRefreshPublisher: AnyPublisher<Void, Never> {
    self.newDataResultPublisher
        .map { _ in }
        .onMainScheduler()
}


var moreDataPublisher: AnyPublisher<[DynamicDisplayType], Never> {
        self.moreDataResultPublisher
            .compactMap { $0 }
            .onMainScheduler()
    }

var endMoreRefreshPublisher: AnyPublisher<Bool, Never> {
    self.moreDataResultPublisher
        .map { _ in kDynamicFileIndex < 5 }
        .merge(with: self.newDataResultPublisher.map { _ in true })
        .onMainScheduler()
}

var toastPublisher: AnyPublisher<String, Never> {
    self.newDataResultPublisher
        .compactMap { result -> String? in
            switch result {
            case .success(_):
                return nil
            case .failure(_):
                return ">_< 数据丢失了!"
            }
        }
        .merge(with: self.moreDataResultPublisher.compactMap { $0 == nil ? ">_< 数据丢失了!" : nil })
        .onMainScheduler()
}
复制代码

# 在 VC 中使用

至此,ViewModelMJRefresh 封装完成。在 VC 中替换 MJRefreshblockpublisher

func eventListen() {

    mjHeader.publisherRefreshing
        .map { _ in }
        .receive(subscriber: viewModel.input.refreshSubscriber)

    mjFooter.publisherRefreshing
        .map { _ in }
        .receive(subscriber: viewModel.input.moreDataSubcriber)
}
复制代码

绑定 viewModel

func bindViewModel() {
    
    // To be contniue 为什么没有使用 subscriber?
    viewModel.output.newDataPublisher
        .sink { [weak self] list in
            self?.reloadData(with: list)
        }
        .store(in: &cancellable)

    viewModel.output.endRefreshPublisher
        .receive(subscriber: mjHeader.subscriber())

    viewModel.output.moreDataPublisher
        .sink { [weak self] list in
            self?.insertData(with: list)
        }
        .store(in: &cancellable)

    viewModel.output.endMoreRefreshPublisher
        .receive(subscriber: mjFooter.subscriber())

    viewModel.output.toastPublisher
        .sink { [weak self] msg in
            self?.toast.showCenter(message: msg)
        }
        .store(in: &cancellable)
}
复制代码

# 补充

文中方法补充:

  • queryNewData

    func queryNewData() {
        kDynamicFileIndex = 0
        self.refreshSubject.send()
        self.topicSubject.send()
    }
    复制代码
  • asAnySubscriber

    extension Subject {
    
        public func asAnySubscriber() -> AnySubscriber<Self.Output, Self.Failure> {
            .init(self)
        }
    }
    复制代码
  • onMainThread

    extension Publisher {
    
        public func onMainScheduler() -> AnyPublisher<Self.Output, Self.Failure> {
            receive(on: RunLoop.main).eraseToAnyPublisher()
        }
    }
    复制代码

UIButton 的拓展

extension UIButton {

    public func subscriber(forTitle state: UIControl.State) -> AnySubscriber<String, Never> {
        let sinkSubscriber = Subscribers.Sink<String, Never> { _ in
        } receiveValue: { [weak self] value in
            self?.setTitle(value, for: state)
        }
        return .init(sinkSubscriber)
    }
}


extension UIControl {

    public func publisher(forAction event: UIControl.Event) -> AnyPublisher<UIControl, Never> {
        ControlPublisher.init(control: self, event: event).eraseToAnyPublisher()
    }
}

复制代码

接下来:

在下一篇博文中我们来研究下 combine 的内存管理,并补充文中 // To be contniue 的部分。

感谢您的阅读,有问题可留言。

分类:
iOS
标签: