我正在参加「启航方案」

1. 现有的一些问题

咱们开发中运用 MVVM模式,在viewModel 中进行输入输出的处理,在controller中进行绑定处理,相似我之前的文章RxSwift学习-24-RxSwift中MVVM的运用 ,那么在实践开发中,如下图的界面怎么优化呢?

Swift中MVVM对于列表中Cell的拆分(上)

比如这个页面,咱们在controllerbindViewModel还是许多逻辑处理,这儿贴下现在的款式

Swift中MVVM对于列表中Cell的拆分(上)

我之前更多的让cell进行展现效果,没有绑定viewModel,数据如下

lazy var versionData: SettingModel = SettingModel.init(title: "查看更新", icon: "me_settting_update", info: "", isPush: false)
  lazy var data: [SectionModel<String, SettingModel>] = {
    return [ 
      SectionModel(model: "2", items: [
        SettingModel.init(title: "个人信息", icon: "me_settting_info", info: "基础信息", isPush: true)
   
        ]),
      SectionModel(model: "2", items: [
        SettingModel.init(title: "联络客服", icon: "me_settting_services", info: R.key.resource.phone, isPush: false)
        ]),
      SectionModel(model: "2", items: [
        SettingModel.init(title: "关于平台", icon: "me_settting_platform", info: "查看", isPush: true, code: "gywm"),
        SettingModel.init(title: "法令声明与隐私方针", icon: "me_settting_law", info: "查看", isPush: true, code: "flsmyszc")
        ]),
      SectionModel(model: "2", items: [
        versionData
        ]),
    ]
  }()

之后运用rx绑定到tableview上,这儿还是运用的传统的模式经过对cell进行setter赋值

let dataSource = BehaviorSubject.init(value: viewModel.data)
    /// dataSource绑定tableview
    let tableViewDataSource = RxTableViewSectionedReloadDataSource<SectionModel<String,SettingModel>>(configureCell: {
      [weak self](dataSource, tab, indexPath, model) -> SettingCell in
      let cell = self?.settingView.tableView.dequeueReusableCell(withIdentifier: SettingCell.description()) as! SettingCell
      cell.data = model
      return cell
    })
   
    dataSource.asDriver(onErrorJustReturn: [])
      .drive(self.settingView.tableView.rx.items(dataSource: tableViewDataSource))
      .disposed(by: disposeBag)
   
    /// 点击model
    self.settingView.tableView.rx.modelSelected(SettingModel.self)
      .subscribe(onNext: {[weak self](model) in
        self?.choseModel(model: model)
     
    }).disposed(by: disposeBag)

之后就是对viewModeloutput进行订阅,以及页面跳转

Swift中MVVM对于列表中Cell的拆分(上)

可是感觉会比较多,也比较乱,而且也没有做到呼应式数据绑定到UI上,我借鉴大神的代码,咱们能够对此进行优化下

2. cell的viewModel

咱们之前自己界说也是运用了RxTableviewDataSource中的SectionModel,只是一些简略的信息。这儿咱们界说cell的viewMode,用于绑定cell的UI

2.1 DefaultTableViewCellViewModel

/// 默认cell的viewmodel
class DefaultTableViewCellViewModel: TableViewCellViewModel {
  /// 标题
  let title = BehaviorRelay<String?>(value: nil)
  /// 概况
  let detail = BehaviorRelay<String?>(value: nil)
  /// 二级概况
  let secondDetail = BehaviorRelay<String?>(value: nil)
  /// 富文本概况
  let attributedDetail = BehaviorRelay<NSAttributedString?>(value: nil)
  /// 图片
  let image = BehaviorRelay<UIImage?>(value: nil)
  /// 图片地址
  let imageUrl = BehaviorRelay<String?>(value: nil)
  /// 提示小红点
  let badge = BehaviorRelay<UIImage?>(value: nil)
  /// 小红点色彩
  let badgeColor = BehaviorRelay<UIColor?>(value: nil)
  /// 隐藏信息开关
  let hidesDisclosure = BehaviorRelay<Bool>(value: false)
}

TableViewCellViewModel基类能够界说一到2个基本的

Swift中MVVM对于列表中Cell的拆分(上)

咱们上图中的cell基本上2种,展现跳转的以及版别跟新小红点的,当然也有switch开关那种比较常用的

Swift中MVVM对于列表中Cell的拆分(上)

2.2 SettingCellViewModel

咱们界说设置中的cell一些常用的数据如下:

class SettingCellViewModel: DefaultTableViewCellViewModel {
 
  init(with title: String, detail: String?, image: UIImage?, hidesDisclosure: Bool) {
    super.init()
    self.title.accept(title)
    self.detail.accept(detail)
    self.image.accept(image)
    self.hidesDisclosure.accept(hidesDisclosure)
  }
}

2.3 SettingBadgeCellViewModel

实践开发中咱们根据需求逻辑判断,增加不同的序列,用于绑定和订阅

class SettingBadgeCellViewModel: DefaultTableViewCellViewModel {
  /// 是否更新
  let isUpdate = BehaviorRelay<Bool>(value: false)
  init(with title: String, detail: String?, image: UIImage?, hidesDisclosure: Bool, isUpdate: Bool) {
    super.init()
    self.title.accept(title)
    self.detail.accept(detail)
    self.image.accept(image)
    self.hidesDisclosure.accept(hidesDisclosure)
    self.isUpdate.accept(isUpdate)
  }
}

2.4 SettingSwitchCellViewModel

switch类型的cell,咱们界说一个开关是否可用,以及switchChanged

class SettingSwitchCellViewModel: DefaultTableViewCellViewModel {
  /// 开关是否可用
  let isEnabled = BehaviorRelay<Bool>(value: false)
  /// 开关
  let switchChanged = PublishSubject<Bool>()
  init(with title: String, detail: String?, image: UIImage?, hidesDisclosure: Bool, isEnabled: Bool) {
    super.init()
    self.title.accept(title)
    self.detail.accept(detail)
    self.image.accept(image)
    self.hidesDisclosure.accept(hidesDisclosure)
    self.isEnabled.accept(isEnabled)
  }
}

咱们界说完cell的viewModel后需求绑定cell,关于基类cell,咱们通常会界说bind办法


open class TableViewCell: UITableViewCell {
  override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    makeUI()
  }
  required public init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  open func makeUI() {
    //选中款式无
    selectionStyle = .none
    contentView.theme_backgroundColor = "TableViewCell.backgroundColor"
  }
}

这儿咱们界说下bind的办法

func bind(to viewModel: TableViewCellViewModel) {
  }

2.5 绑定

之前咱们通常处理cell的方式是对model进行赋值经过setter办法

var data: SettingModel? {
    didSet {
      iconImgView.image = UIImage.getBundleImage(imageName: data?.icon ?? "")
      titleLabel.text = data?.title
      infoLabel.text = data?.info
      arrowImgView.isHidden = !(data?.isPush ?? true)
      infoLabel.textColor = data?.isPush ?? false ? UIColor.blackWithAlpha(0.45) : R.color.color10AA89
      if data?.title == "查看更新" {
        infoLabel.isHidden = true
        if let newVersion = data?.update?.version, newVersion.count > 0 {
          updatingLabel.isHidden = false
          if let info = Bundle.main.infoDictionary, let appVersion = info["CFBundleShortVersionString"] as? String {
            let comparsion = appVersion.compare(newVersion, options: .numeric, range: nil, locale: nil)
            if comparsion == .orderedAscending {
              updatingLabel.text = " 新版别 "
              updatingLabel.layer.borderColor = UIColor(hexString: "E64340").cgColor
              updatingLabel.textColor = UIColor(hexString: "E64340")
            } else {
              updatingLabel.text = "已是最新版别"
              updatingLabel.layer.borderColor = UIColor.clear.cgColor
              updatingLabel.textColor = UIColor.blackWithAlpha(0.45)
            }
          }
        } else {
          updatingLabel.isHidden = true
        }
      } else {
        infoLabel.isHidden = false
        updatingLabel.isHidden = true
      }
    }
  }

咱们在 DefaultTableViewCell中绑定

override func bind(to viewModel: TableViewCellViewModel) {
    super.bind(to: viewModel)
    guard let viewModel = viewModel as? DefaultTableViewCellViewModel else { return }
    viewModel.title.asDriver().drive(titleLabel.rx.text).disposed(by: rx.disposeBag)
    viewModel.title.asDriver().replaceNilWith("").map { $0.isEmpty }.drive(titleLabel.rx.isHidden).disposed(by: rx.disposeBag)
    viewModel.detail.asDriver().drive(detailLabel.rx.text).disposed(by: rx.disposeBag)
    viewModel.detail.asDriver().replaceNilWith("").map { $0.isEmpty }.drive(detailLabel.rx.isHidden).disposed(by: rx.disposeBag)
    viewModel.secondDetail.asDriver().drive(secondDetailLabel.rx.text).disposed(by: rx.disposeBag)
    viewModel.secondDetail.asDriver().replaceNilWith("").map { $0.isEmpty }.drive(secondDetailLabel.rx.isHidden).disposed(by: rx.disposeBag)
    viewModel.attributedDetail.asDriver().drive(attributedDetailLabel.rx.attributedText).disposed(by: rx.disposeBag)
    viewModel.attributedDetail.asDriver().map { $0 == nil }.drive(attributedDetailLabel.rx.isHidden).disposed(by: rx.disposeBag)
    viewModel.badge.asDriver().drive(badgeImageView.rx.image).disposed(by: rx.disposeBag)
    viewModel.badge.map { $0 == nil }.asDriver(onErrorJustReturn: true).drive(badgeImageView.rx.isHidden).disposed(by: rx.disposeBag)
    viewModel.badgeColor.asDriver().drive(badgeImageView.rx.tintColor).disposed(by: rx.disposeBag)
    viewModel.hidesDisclosure.asDriver().drive(rightImageView.rx.isHidden).disposed(by: rx.disposeBag)
    viewModel.image.asDriver().filterNil()
      .drive(leftImageView.rx.image).disposed(by: rx.disposeBag)
    viewModel.imageUrl.map { $0?.url }.asDriver(onErrorJustReturn: nil).filterNil()
      .drive(leftImageView.rx.imageURL).disposed(by: rx.disposeBag)
    viewModel.imageUrl.asDriver().filterNil()
      .drive(onNext: { [weak self] (url) in
        self?.leftImageView.hero.id = url
      }).disposed(by: rx.disposeBag)
  }

这儿我是在原有基础上的cell修改,就直接界说了

func bind(to viewModel: TableViewCellViewModel) {
    guard let viewModel = viewModel as? SettingBadgeCellViewModel else { return }
    /// 标题
    viewModel.title.asDriver().drive(titleLabel.rx.text).disposed(by: cellDisposeBag)
    /// 图标
    viewModel.image.asDriver().drive(iconImgView.rx.image).disposed(by: cellDisposeBag)
    /// 概况
    viewModel.detail.asDriver().drive(infoLabel.rx.text).disposed(by: cellDisposeBag)
    /// 箭头
    viewModel.hidesDisclosure.asDriver().drive(arrowImgView.rx.isHidden).disposed(by: cellDisposeBag)
    /// 高亮色彩
    viewModel.highlightColor.asDriver().drive(infoLabel.rx.textColor).disposed(by: cellDisposeBag)
    /// 跟新款式
    viewModel.isUpdate.asDriver().drive(infoLabel.rx.isHidden).disposed(by: cellDisposeBag)
    viewModel.isUpdate.asDriver().map{!$0}.drive(updatingLabel.rx.isHidden).disposed(by: cellDisposeBag)
    /// 是否跟新
    viewModel.isNewest.subscribe(onNext: {[unowned self] isShow in self.isChangeUpdateStyle(isUpdate: isShow)} ).disposed(by: cellDisposeBag)
  }
  func isChangeUpdateStyle(isUpdate:Bool?) {
    guard let isUpdate = isUpdate else {return}
    if isUpdate {
      updatingLabel.text = " 新版别 "
      updatingLabel.layer.borderColor = UIColor(hexString: "E64340").cgColor
      updatingLabel.textColor = UIColor(hexString: "E64340")
    } else {
      updatingLabel.text = "已是最新版别"
      updatingLabel.layer.borderColor = UIColor.clear.cgColor
      updatingLabel.textColor = UIColor.blackWithAlpha(0.45)
    }
  }

当然关于咱们实践开发中或许会有增加字段状况,咱们也能够在viewModel中增加model参数或者字段,之后运用convenience初始化新的办法

Swift中MVVM对于列表中Cell的拆分(上)

3. 界说分组SettingsSection

之前咱们运用的是默认sectionModel,这儿咱们自界说

import UIKit
import RxDataSources
import RxOptional
/// 分组类型
enum SettingsSection {
  case setting(title: String, items: [SettingsSectionItem])
}
enum SettingsSectionItem {
  /// 个人信息
  case userInfo(viewModel: SettingBadgeCellViewModel)
  /// 客服
  case service(viewModel: SettingBadgeCellViewModel)
  /// 关于
  case aboutPlatform(viewModel: SettingBadgeCellViewModel)
  case privacyPolicy(viewModel: SettingBadgeCellViewModel)
 
  /// 更新
  case update(viewModel: SettingBadgeCellViewModel)
}
extension SettingsSectionItem: IdentifiableType {
  typealias Identity = String
  var identity: Identity {
    switch self {
    case .userInfo(viewModel: let viewModel),
    .service(viewModel: let viewModel),
    .aboutPlatform(viewModel: let viewModel),
    .privacyPolicy(viewModel: let viewModel),
    .update(viewModel: let viewModel): return viewModel.title.value ?? ""
    }
  }
}
extension SettingsSectionItem: Equatable {
  static func == (lhs: SettingsSectionItem, rhs: SettingsSectionItem) -> Bool {
    return lhs.identity == rhs.identity
  }
}
extension SettingsSection: AnimatableSectionModelType, IdentifiableType {
  typealias Item = SettingsSectionItem
  typealias Identity = String
  var identity: Identity { return title }
  var title: String {
    switch self {
    case .setting(let title, _): return title
    }
  }
  var items: [SettingsSectionItem] {
    switch self {
    case .setting(_, let items): return items.map {$0}
    }
  }
  init(original: SettingsSection, items: [Item]) {
    switch original {
    case .setting(let title, let items): self = .setting(title: title, items: items)
    }
  }
}

经过枚举咱们能够直接增加,变化比较便利,便利后续拓宽。

4. 小结

这儿咱们经过cell绑定自己的viewModel,这样使得能够经过数据刷新界面,经过区别不同的cell,关于原有的viewModel让咱们控制器的逻辑拆分一部分,也是更纯粹的呼应式体现。经过枚举界说分组以及不同类型item,也是便利咱们进行拓宽,关于控制器的ViewModel和controller的绑定,在下篇说明。