對沸點頁面仿寫的補充-Combine

語言: CN / TW / HK

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 中,封裝如下:

```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 {} 的方式,如下:

```Swift 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> 型別,如:

```Swift // RxSwift var moreData: Observable { get }

// Combine var moreData: AnyPublisher { 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 理由是用的而地方太少,使用手寫約束佈局替換

```Swift // 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

感謝您的閱讀。