我正在参加「启航方案」
1. 现有的一些问题
咱们开发中运用 MVVM
模式,在viewModel
中进行输入输出的处理,在controller
中进行绑定处理,相似我之前的文章RxSwift学习-24-RxSwift中MVVM的运用 ,那么在实践开发中,如下图的界面怎么优化呢?
比如这个页面,咱们在controller
中bindViewModel
还是许多逻辑处理,这儿贴下现在的款式
我之前更多的让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)
之后就是对viewModel
的output
进行订阅,以及页面跳转
可是感觉会比较多,也比较乱,而且也没有做到呼应式数据绑定到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个基本的
咱们上图中的cell基本上2种,展现跳转的以及版别跟新小红点的,当然也有switch
开关那种比较常用的
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
初始化新的办法
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的绑定,在下篇说明。