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

3,245 阅读4分钟

我正在参加「掘金·启航计划」

1. 现有的一些问题

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

image.png

比如这个页面,我们在controllerbindViewModel还是很多逻辑处理,这里贴下现在的样式

image.png

我之前更多的让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进行订阅,以及页面跳转

image.png

但是感觉会比较多,也比较乱,而且也没有做到响应式数据绑定到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个基本的

image.png

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

image.png

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初始化新的方法

image.png

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的绑定,在下篇说明。