MVVM UICollectionView/UITableView

最近銀行對(duì)項(xiàng)目提出了新的要求,80%的Unit Test成為了硬指標(biāo)。特別是在喪失了Uber合作以后,為了盡快挽回名聲,對(duì)技術(shù)人員提出了更高的要求。因?yàn)闅v史原因,項(xiàng)目有大量的C和OC混編的代碼,而且長(zhǎng)待七年的代碼量不是一兩天能夠完成了。在長(zhǎng)達(dá)一個(gè)月的扯皮以后,標(biāo)準(zhǔn)降低為了近一年以后的新代碼必須達(dá)標(biāo)。近一年的代碼主要都是swift項(xiàng)目,雖然大部分都是MVC項(xiàng)目,但是改造成MVVM也并不是什么特別困難的事情。

個(gè)人對(duì)于MVVM一直相對(duì)比較抵觸,因?yàn)槲覀€(gè)人工作的內(nèi)容有大量的prototype和POC,因此MVVM會(huì)顯著增加代碼量并且降低開發(fā)速度。而且我覺得網(wǎng)上很多人言MVVM必及automated UI是完全沒有道理的。雖然reactiveCocoa之類的函數(shù)式編程是體現(xiàn)了MVVM的思想,但是MVVM和他們是不可以直接畫上等號(hào)的。我個(gè)人對(duì)MVVM的理解在于,MVVM可以比較徹底的將UI和業(yè)務(wù)邏輯分離。UI和業(yè)務(wù)邏輯分離的好處是顯而易見的:

  1. 對(duì)于可以復(fù)用的UI module, 能夠輕松做到drag&drop;
  2. 對(duì)于業(yè)務(wù)邏輯的替換相對(duì)比較容易,比如從第三方網(wǎng)絡(luò)庫切換到自己的網(wǎng)絡(luò)庫;
  3. Unit Test!?。。?duì)于一個(gè)一兩千行的ViewController進(jìn)行Unit Test絕對(duì)是一個(gè)災(zāi)難。

今天要講的UICollectionView就同時(shí)體現(xiàn)了這三點(diǎn)好處。

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){}
}

上面是我為公司設(shè)計(jì)的第二個(gè)版本,符合swift 3.0和POP思想,這個(gè)代碼原本是我貼在公司設(shè)計(jì)模板上的。這個(gè)一個(gè)簡(jiǎn)單的可復(fù)用final 類。這個(gè)類的作用是管理整個(gè)項(xiàng)目里面的各種UICollectionView。

基本思想是通過符合ViewModel的存在把cell的UI和UserInteraction從ViewController里面分離出去,然后通過上面這個(gè)工廠類進(jìn)行拼裝。這樣我們就可以對(duì)每一個(gè)ViewModel去寫Unit Test。

我們可以看一下具體如何使用這個(gè)類,現(xiàn)在我們假設(shè)我們有一個(gè)cell需要展示一個(gè)叫做Account的類:

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

所以我先建一個(gè)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
        }
    }
}

這是一個(gè)十分簡(jiǎn)單的cell,只有一個(gè)label,并且沒有任何IBAction,很好?,F(xiàn)在作為Cell的建立者,我有義務(wù)保證這個(gè)cell所對(duì)應(yīng)的ViewModel能夠處理我的需求,那么我的需求是我有一個(gè)titleLabel需要String類型的數(shù)據(jù)。所以,我發(fā)布一個(gè)protocol來保證我的viewmodel一定要提供給我這個(gè)數(shù)據(jù)。 同時(shí),在我拿到這個(gè)數(shù)據(jù)之后,我通過父類的Generic Method來實(shí)現(xiàn)UI的更新。需要注意的是,因?yàn)檫@個(gè)父類的方法是不安全的,因?yàn)門是沒有任何限制的,所以我們需要自己做一個(gè)安全性檢查。

下面,我們就應(yīng)該針對(duì)這個(gè)cell去設(shè)計(jì)一個(gè)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}
}

因?yàn)閕OS10之前,UICollectionView是不支持自動(dòng)size的,所以作為ViewModel,這個(gè)ViewModel是有義務(wù)提供cell的viewSize的。Type這個(gè)屬性是用于工廠類進(jìn)行數(shù)據(jù)類型比對(duì)的。這個(gè)類也非常簡(jiǎn)單,相信大家一目了然。

好,現(xiàn)在我們看一下如何在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
在這個(gè)project之中我還demo了如何使用Viewmodel去處理IBAction例如UIButton的action。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容