对沸点页面仿写的补充-Combine

1,270 阅读2分钟

640.PNG

# 前言

本文就是 上篇 文章结尾所提到的替换 RxSwift

本文篇幅也会太长,涉及内容也仅仅是 Combine 的使用,您完全可以将此 demo 当入手 Combine 的上手实验。文末会附上 Combine 学习的网址,同样有文中源码附上。

老传统了,看图吧

Simulator Screen Shot - iPhone 13 - 2022-04-12 at 12.31.03.png

# 网络层替换

添加对应的 Moya/Combine 后,对网络层采用 网络补充 中的封装方式,同样提供 request()memoryCacheIn(_)onStorage(_:atKeyPath:onDisk:) 方法。现将缓存相关的 extension 抽离到 Moya+XTCache.swift 中,封装如下:

extension TargetType {

    public func request() -> AnyPublisher<Response, MoyaError> {
        xtProvider.requestPublisher(.target(self))
    }

    /// 使用时间缓存策略, 内存中有数据就不请求网络
    public func memoryCacheIn(_ seconds: TimeInterval = 180) -> TargetOnMemoryCache {
        TargetOnMemoryCache(target: self, cacheTime: seconds)
    }

    /// 读取磁盘缓存, 一般用于启动时先加载数据, 而后真正的读取网络数据
    public func onStorage<T: Codable>(_ type: T.Type, atKeyPath keyPath: String? = nil, onDisk: ((T) -> ())?) -> TargetOnDiskStorage<T> {
        let diskStore: TargetOnDiskStorage<T> = .init(target: self, keyPath: keyPath)
        if let storage = diskStore.readDiskStorage(type) { onDisk?(storage) }
        return diskStore
    }
}

对于 memoryCache 的处理没有采用,extension AnyPublisher {} 的方式,如下:

extension AnyPublisher {

    func request() -> AnyPublisher<Response, MoyaError> where Output == (TargetType, TimeInterval), Failure == MoyaError {
        flatMap { tuple -> AnyPublisher<Response, MoyaError> in
            let target = tuple.0
            let cacheKey = target.cacheKey
            if let response = cachedResponse(for: cacheKey) {
                return CurrentValueSubject(response).eraseToAnyPublisher()
            }

            return target.request().map { response -> Response in
                 kMemoryStroage.setObject(response, forKey: cacheKey, expiry: .seconds(seconds))
                 return response
            }
            .eraseToAnyPublisher()
        }
        .eraseToAnyPublisher()
    }
}

使用 CurrentValueSubject 保证在被订阅时是有值的,具体的 RxSwiftCombine 对照表见文末 补充 部分。

# ViewModel 替换

CombinePubliser 指定了 Failure,因此对于我们在 RxSwift 中不会发送 errorObservable 来说,可以清晰的定义为 AnyPublisher<SendType, Never> 类型,如:

// RxSwift
var moreData: Observable<DynamicDisplayModel> { get }

// Combine
var moreData: AnyPublisher<DynamicDisplayModel, Never> { get }

CombinePassthroughSubject<SendType, Never> 替换 RxSwiftPublishSubject<SendType>

这里要注意的一点就是 CombineflatMap 操作符并不如 RxSwift 中那么好用,如下图展示错误所示:

flatmap.png

AnyPublisher<XTListResultModel, MoyaError>iOS 13 版本不支持转换为 AnyPublisher<Result<XTListResultModel, Error>, Never>iOS 14 之后才能使用,这一部分我们使用 map 替换,如下图:

map.png

# 补充

1. Combine 学习网址:
  1. Combine 入门导读

  2. Rxswift与Combine对照表

  3. Using Combine-中文

建议顺序阅读

2. Combine 补充

receive(on: main)subscribe(on: main) 的区别。receive(on: main) 保证 .sink {} 中的代码是在 main thread 中执行。 具体可参考文章 subscribe-revcive

3. 自定义 Publisher

demoTextureDemoViewController.swift 中有一个自定义的 JsonDataPublisher。同样在 Moya/CombineNuke/Combine 中也有自定义的 Publisher 可供学习。

4. 移除 Kingfisher,SnapKit

demo 的 MOON 分支移除了 SnapKit 理由是用的而地方太少,使用手写约束布局替换

// SnapKit
button.snp.makeConstraints { make in
    make.left.equalToSuperview()
    make.top.equalToSuperview()
    make.bottom.equalToSuperview()
    make.width.equalToSuperview().dividedBy(CGFloat(count))
}

// AutoLayout
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    button.leftAnchor.constraint(equalTo: previousButton?.rightAnchor ?? self.leftAnchor),
    button.topAnchor.constraint(equalTo: self.topAnchor),
    button.bottomAnchor.constraint(equalTo: self.bottomAnchor),
    button.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1/CGFloat(count))
])

使用 Nuke 替换 Kingfisher 纯个人兴趣。更多 Nuke说明,参照 Nuke-DocNuke-demo

感谢您的阅读。