MVVM UICollectionView/UITableView

最近银行对项目提出了新的要求,80%的Unit Test成为了硬指标。特别是在丧失了Uber合作以后,为了尽快挽回名声,对技术人员提出了更高的要求。因为历史原因,项目有大量的C和OC混编的代码,而且长待七年的代码量不是一两天能够完成了。在长达一个月的扯皮以后,标准降低为了近一年以后的新代码必须达标。近一年的代码主要都是swift项目,虽然大部分都是MVC项目,但是改造成MVVM也并不是什么特别困难的事情。

个人对于MVVM一直相对比较抵触,因为我个人工作的内容有大量的prototype和POC,因此MVVM会显著增加代码量并且降低开发速度。而且我觉得网上很多人言MVVM必及automated UI是完全没有道理的。虽然reactiveCocoa之类的函数式编程是体现了MVVM的思想,但是MVVM和他们是不可以直接画上等号的。我个人对MVVM的理解在于,MVVM可以比较彻底的将UI和业务逻辑分离。UI和业务逻辑分离的好处是显而易见的:

  1. 对于可以复用的UI module, 能够轻松做到drag&drop;
  2. 对于业务逻辑的替换相对比较容易,比如从第三方网络库切换到自己的网络库;
  3. Unit Test!!!!对于一个一两千行的ViewController进行Unit Test绝对是一个灾难。

今天要讲的UICollectionView就同时体现了这三点好处。

import UIKit

//Why VVVVVVVVVVVM? My mother taught me a long enough title will catch eyes.
//Why class? -----------------  You have to be a class to adopt UICollectionViewDataSource 🤷‍♀️
//                           🔝
protocol FactoryDataSource:class{
    //This holds data coming from each controller.
    var dataContainer:[Any]{get}
}

final class CollectionFactory:NSObject {
    static let shared = CollectionFactory()
    
    //Why vms become private now? Because this can become super duper ugly if you have a lot of viewmodels
    //Thanks for keeping my factory clean
    fileprivate var vms:[Tags] = [Tags]()
    
    weak var delegate:FactoryDataSource?
    
    /*
     Why private? Educate your user, I create singlton, so you have to use it.
     Under protest? Sorry, I am psycho control freak.
     */
    private override init() {}
    
    //Hotel reception: please register your view model here, yes, all of them, "where is your ID?"
    func registerViewModel(vm:Tags){
        let existed = vms.contains {object_getClassName($0.type) == object_getClassName(vm.type)}
        if !existed {
            vms.append(vm)
        }
    }
}

extension CollectionFactory:UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        //In case someone forgets to set delegate, don't laugh, it could happen to anyone, not only baby dev.
        guard let _ = delegate else {return 0}
        return delegate!.dataContainer.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        for vm in vms {
            if object_getClassName(vm.type) == object_getClassName(delegate!.dataContainer[indexPath.row]) {
                vm.updateData(delegate!.dataContainer[indexPath.row])
                //If you forget to subclassWFCollectionCell, App will crash because of next line!!!!
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier:vm.identifier, for: indexPath) as! WFCollectionCell
                cell.configureCell(t: vm)
                return cell
            }
        }
        return UICollectionViewCell()
    }
}

extension CollectionFactory:UICollectionViewDelegateFlowLayout{
    
    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        sizeForItemAt indexPath: IndexPath) -> CGSize {
        for vm in vms{
            if object_getClassName(vm.type) == object_getClassName(delegate!.dataContainer[indexPath.row]){
                return vm.viewSize
            }
        }
        return CGSize.zero
    }
}

typealias Tags = WFCollectionCellDataSource&WFCollectionCellDelegate

protocol WFCollectionCellDataSource{
    //This is your cell size
    var viewSize:CGSize{get}
    //This is your cell reuse identifier
    var identifier:String{get}
    //This is your cell's associated type
    //I know there is AssociatedType for protocol, it just doesn't work
    var type:Any{get}

    /*
     Useage:
     class VM:Tags {
        private var realData:Int = 0
        func updateData(_ data: Any) {
            if data is Int {
            self.realData = data as! Int
        }
     }
     */
    func updateData(_ data:Any)
}

//I have this empty protocol here for you to add any methods you need for p164 here.
//Why we have two separate protocols when we can just have one?
//Because I like separating them, bite me?
protocol WFCollectionCellDelegate {}

/*
 This class should be super class your cell instead of UICollectionViewCell
 I know some of you might think this retarded guy create a class intead of write an extension to UICollectionview?
 Yes, because you have to override this method. Because we don't have a binder.
 */
class WFCollectionCell:UICollectionViewCell {
    func configureCell<T>(t:T){}
}

上面是我为公司设计的第二个版本,符合swift 3.0和POP思想,这个代码原本是我贴在公司设计模板上的。这个一个简单的可复用final 类。这个类的作用是管理整个项目里面的各种UICollectionView。

基本思想是通过符合ViewModel的存在把cell的UI和UserInteraction从ViewController里面分离出去,然后通过上面这个工厂类进行拼装。这样我们就可以对每一个ViewModel去写Unit Test。

我们可以看一下具体如何使用这个类,现在我们假设我们有一个cell需要展示一个叫做Account的类:

class Account {
    var name:String
    init(n:String) {
        self.name = n
    }
}

所以我先建一个Cell:

import UIKit

/*
 Everytime you create one cell, make sure you follow steps:
    1. Make sure you use Space instead of Tab to insert space
    2. Instead of subclassing UICollectionViewCell, subclass WFColletionCell
    3. Everytime you create one cell, you should also create one associated protocol.
       Remember! YOU are the one who is responsible for guaranteeing your protocol has enough
       properties to populate UI and methods to handle User Interaction!!!
*/
protocol StringCellDataSource {
    var title:String{get}
}

class StringCollectionViewCell: WFCollectionCell {

    @IBOutlet weak var titleLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }
    
    //Don't forget to override this method! This method is where magic happens.
    override func configureCell<T>(t: T) {
        //Since we don't have a Binder class and Your leads (such as Tabari(Gaussian Blur applied) and Marc(Gaussian Blur applied)) don't beleive in "Binder"
        //You, again, become the one who is responsible for binding them together
        if t is StringCellVM {
            titleLabel.text = (t as! StringCellVM).title
        }
    }
}

这是一个十分简单的cell,只有一个label,并且没有任何IBAction,很好。现在作为Cell的建立者,我有义务保证这个cell所对应的ViewModel能够处理我的需求,那么我的需求是我有一个titleLabel需要String类型的数据。所以,我发布一个protocol来保证我的viewmodel一定要提供给我这个数据。 同时,在我拿到这个数据之后,我通过父类的Generic Method来实现UI的更新。需要注意的是,因为这个父类的方法是不安全的,因为T是没有任何限制的,所以我们需要自己做一个安全性检查。

下面,我们就应该针对这个cell去设计一个ViewModel了:

import UIKit

class StringCellVM:Tags {
    
    var viewSize:CGSize{return CGSize(width: 335, height: 66)}
    var identifier:String{return "stringCell"}
    var type: Any = Account(n:"Sample")
    fileprivate var realData:Account!
    
    func updateData(_ data: Any) {
        if data is Account {
            self.realData = data as! Account
        }
    }
}

extension StringCellVM:StringCellDataSource {
    var title:String{return realData.name}
}

因为iOS10之前,UICollectionView是不支持自动size的,所以作为ViewModel,这个ViewModel是有义务提供cell的viewSize的。Type这个属性是用于工厂类进行数据类型比对的。这个类也非常简单,相信大家一目了然。

好,现在我们看一下如何在ViewController里面使用ViewModel:

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var myBeautifulList: UICollectionView!

    //MARK: Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // 1. Register all xibs
        myBeautifulList.register(UINib(nibName: "StringCollectionViewCell", bundle: nil),
                                 forCellWithReuseIdentifier: "stringCell")
        myBeautifulList.register(UINib(nibName: "IntegerCollectionViewCell", bundle: nil),
                                 forCellWithReuseIdentifier: "IntCell")
        // 2. Set Delegate
        CollectionFactory.shared.delegate = self
        
        // 3. Register all xibs' associated view models
        CollectionFactory.shared.registerViewModel(vm: StringCellVM())
        CollectionFactory.shared.registerViewModel(vm: IntegerCellVM())
        
        // 4. Hook up
        myBeautifulList.dataSource = CollectionFactory.shared
        myBeautifulList.delegate = CollectionFactory.shared
        
        // 5. Bad Apple Code
        automaticallyAdjustsScrollViewInsets = false
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

extension ViewController:FactoryDataSource{
    //This is simulate data from server, so it can have different types of class coverted from Json
    var dataContainer:[Any]{return [Account(n:"One"),1,Account(n:"Two"),2,Account(n:"Three"),3,Account(n:"Four"),4,Account(n:"Five"),5]}
}

完整的project:https://github.com/LHLL/MVVMSample
在这个project之中我还demo了如何使用Viewmodel去处理IBAction例如UIButton的action。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容