我正在参加「掘金·启航计划」
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的绑定,在下篇说明。