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 使用过程中的利弊吧。