iOS使用Texture和Rxswitf進行MVVM架構的沸點頁面仿寫

語言: CN / TW / HK

0 3.JPEG

老習慣了的先展示下結果吧:

# 技術路線

# 三方庫選擇:

  • Texture
  • Moya
  • RxSwift
  • Kingfisher
  • Lantern : 照片瀏覽器
  • Toast-Swift : toast 提示
  • etc...

# 整體架構

使用 Texture 進行的介面元素搭建,RxSwift 實現的 VM 資料流驅動 View 更新,Moya + Rx 的網路請求。

# Texture

對於 Texture 的使用其實上手難度不如我預想的大,官方的大量例子,和一些優秀的技術部落格都能讓我們很好地接入 Texture

佈局:可以通過在網頁(ASDK🐸)上以玩樂的形式上手 FlexBox 佈局。

原理與使用

這裡看一個佈局點贊使用者頭像列表的佈局吧:

```Swift

// Title private lazy var titleNode: ASTextNode = { let node = ASTextNode() let attr: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 12, weight: .regular), .foregroundColor: UIColor.XiTu.digCount ] node.attributedText = NSAttributedString(string: "等人贊過", attributes: attr) return node }()

// 佈局 override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {

    let imageHeihgt = constrainedSize.min.height - 1
    imageNode1.style.preferredSize = CGSize(width: imageHeihgt, height: imageHeihgt)
    imageNode2.style.preferredSize = CGSize(width: imageHeihgt, height: imageHeihgt)
    imageNode3.style.preferredSize = CGSize(width: imageHeihgt, height: imageHeihgt)
    imageNode1.cornerRadius = imageHeihgt / 2
    imageNode2.cornerRadius = imageHeihgt / 2
    imageNode3.cornerRadius = imageHeihgt / 2

    let overlayWidth: CGFloat = 5
    let inset1 = ASInsetLayoutSpec(insets: .only(.right, value: 2 * (imageHeihgt - overlayWidth)), child: imageNode1)
    let inset2 = ASInsetLayoutSpec(insets: .only(.horizontal, value: imageHeihgt - overlayWidth), child: imageNode2)
    let inset3 = ASInsetLayoutSpec(insets: .only(.left, value:2 * (imageHeihgt - overlayWidth) ), child: imageNode3)
    let overlay1 = ASOverlayLayoutSpec(child: inset1, overlay: inset2)
    let overlay = ASOverlayLayoutSpec(child: overlay1, overlay: inset3)

    let hStack = ASStackLayoutSpec.horizontal()
    hStack.spacing = 5
    hStack.alignItems = .center
    hStack.children = [overlay, titleNode]

    return ASInsetLayoutSpec(insets: .only(.left, value: 7), child: hStack)
}

```

# MVVMRxSwift

Demo 中並沒有使用 雙向資料繫結,未對 RxSwift 進行運算子過載,也未使用上次的 RxTableViewDataSource。整體程式碼可讀性應該會有所提高吧,適合 RxSwift 入門學習。

ViewModelVM 層使用協議區分了 inputoutput,介面隔離:

```Swift protocol DynamicListViewModelInputs {

func viewDidLoad()

func refreshDate()

func moreData(with cursor: String)

// FIXED: - 以下 view 層介面需要根據產品的邏輯而定. 例如: 是否要處理一部分埋點之類的額外操作

/// 檢視詳情     func showDetail()

/// 檢視點贊使用者     func diggUserClick() }

protocol DynamicListViewModelOutputs {

//var willRefreshData: Observable { get }

var refreshData: Observable { get }

var moreData: Observable { get }

var endRefresh: Observable { get }

var hasMoreData: Observable { get }

var showError: Observable { get } }

protocol DynamicListViewModelType {     var input: DynamicListViewModelInputs { get }     var output: DynamicListViewModelOutputs { get } }

final class DynamicListViewModel: DynamicListViewModelType, DynamicListViewModelInputs, DynamicListViewModelOutputs {

var input: DynamicListViewModelInputs { self }

var output: DynamicListViewModelOutputs { self } // ect... } ```

View(VC) 中的使用:

```Swift extension DynamicListViewController {     func eventListen() {         self.mjHeader.refreshingBlock = { [unowned self] in             self.viewModel.input.refreshDate()         }

self.mjFooter.refreshingBlock = { [unowned self] in             self.viewModel.input.moreData(with: self.dataSource.nextCursor)         }     } }

// MARK: - 繫結 viewModel

extension DynamicListViewController {

func bindViewModel() {

    viewModel.output.refreshData.subscribe(onNext: { [weak self] wrappedModel in
        self?.dataSource.newData(from: wrappedModel)
        self?.tableNode.reloadData()
    }).disposed(by: disposeBag)

    viewModel.output.moreData.subscribe(onNext: { [weak self] wrappedModel in
        if let insertIndexPath = self?.dataSource.moreData(from: wrappedModel), !insertIndexPath.isEmpty {
            self?.tableNode.insertRows(at: insertIndexPath, with: UITableView.RowAnimation.automatic)
        }
    }).disposed(by: disposeBag)

    // etc...
}

} ```

DataSource:對 ASTableNode (UITableView) 拆分出了 DataSource 來儲存和處理 Model、遵守 ASTableDataSource 協議,將 VM 層的職責進一步的細分,便於維護。

這裡就說這麼多吧,更多的是在 cellNodecellNodeModel 裡面的邏輯處理,和此類似。

備註:最近被大佬教育過不敢自稱是 MVVM 架構,但資料處理層仍使用了 XxxNodeMode/ XxxViewModel 的名稱。

Model: 直接使用 structCodable 進行 JSON 解析。注意,如果使用 UITableView 實現類似的介面效果可能需要在 model 中補充額外的儲存屬性和 Decoder 邏輯程式碼。

# Moya

掘金裡相關技術文章也不少,這裡不做說明了。

# 相關知識點

# ASImageNode + Kingfrisher

對於 ASImageNode 使用第三方圖片庫其實並不困難,特別是對於面向協議封裝的 Kingfirsh,這裡簡單的擴充套件一下吧:

```Swift import Kingfisher import AsyncDisplayKit

extension ASImageNode: KingfisherCompatible {}

public extension KingfisherWrapper where Base: ASImageNode {

func setImage(
    with source: Resource?,
    placeholder: UIImage? = nil,
    failureImage: UIImage? = nil,
    options: KingfisherOptionsInfo? = nil,
    progressBlock: DownloadProgressBlock? = nil)
{
    guard let source = source else {
        self.base.image = placeholder ?? failureImage
        return
    }

    KingfisherManager.shared.retrieveImage(with: source, options: options, progressBlock: progressBlock, downloadTaskUpdated: nil) { result in
        switch result {
        case .success(let retrieveResult):
            self.base.image = retrieveResult.image
        case .failure(_):
            self.base.image = failureImage ?? placeholder
        }
    }
}

} ```

備註:老久沒看 SDWebImage 的原始碼了,最近才發現 SDWebImage 內部也開始使用 協議實現協議的預設類 重構了,是我太久沒關注的原因嗎😅?

# 其他

  • 使 ASControl 支援 RxSwift
  • 正則(regx)表示式在 Swift 中的使用
  • 名稱空間的建立
  • etc...

具體見原始碼吧。

# 關於原始碼

原始碼中已經將介面進行了移除,如果您有介面抓取能力(太簡單的操作了)請在

  1. SceneDelegate.swift 中如下程式碼將 rootVC 更換為 HomeTabBarController

    ```Swift func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let _ = (scene as? UIWindowScene) else { return }

    guard let windowScene = (scene as? UIWindowScene) else { return }
    window = UIWindow.init(frame: windowScene.coordinateSpace.bounds)
    window?.windowScene = windowScene
    
    //let rootVC = HomeTabBarController()
    //let rootVC = UINavigationController(rootViewController: SimpleRegxViewController())
    let rootVC = UINavigationController(rootViewController: TextureDemoViewController())
    window?.rootViewController = rootVC
    window?.makeKeyAndVisible()
    
    UIWindow.setupLayoutFitInfo()
    

    } `` 2.XTNetworkService.swift中將jjBaseUrlvar path{xxx}` 替換到對應的介面。

如果僅是學習 Texture 佈局等,原始碼中也附帶了 xxx.json 檔案供您本地使用(直接執行就好了)。

# 絮叨

2020年掘金的iOS客戶端介面還在使用 Texture ,其中的沸點頁面令初見 Texture 的我大吃一驚,也是從此開始接觸並使用 Texture,其和 CSS 相似的 FlexBox 佈局思想令人著迷。如今掘金的iOS版本經歷了大的重構,去除了很多第三方庫,這其中就包括 Texture,常用App中使用 Texture 的廠家又少了一個😞。這裡也希望掘金iOS團隊能夠分享一下 Texture 使用過程中的利弊吧。