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

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

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

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYi1u4ne' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片