MVVM UICollectionView/UITableView

9305 ワード

最近、銀行はプロジェクトに対して新しい要求を提出し、80%のUnit Testがハード指標になった.特にUberの協力を失った後、名声を取り戻すために、技術者にもっと高い要求を出した.歴史的な理由で、プロジェクトには大量のCとOCが混在したコードがあり、7年も待つコード量は1、2日で完成するわけではありません.1ヶ月にわたるいたずらの後、標準の低下は1年近く後の新しいコードのために基準を達成しなければならない.近年のコードは主にswiftプロジェクトであり,ほとんどがMVCプロジェクトであるが,MVVMに改造することは特に困難なことではない.
個人はMVVMに対してずっと相対的に抵抗して、私の個人の仕事の内容は大量のprototypeとPOCがあるため、MVVMはコード量を著しく増加して開発速度を下げます.また、ネット上でMVVM必及automated UIと言う人が多いのは全く無理だと思います.reactiveCocoaのような関数式プログラミングはMVVMの思想を体現しているが,MVVMと彼らは直接等号を描くことはできない.私個人のMVVMに対する理解は、MVVMがUIとビジネスロジックを比較的徹底的に分離できることです.UIとビジネスロジックの分離のメリットは明らかです.
  • 多重化可能なUI moduleに対してdrag&dropを容易に行うことができる.
  • は、サードパーティのネットワークライブラリから自分のネットワークライブラリに切り替えるなど、ビジネスロジックの置き換えが比較的容易である.
  • Unit Test!!!!1、2千行のViewControllerに対してUnit Testを行うのは絶対に災難です.

  • 今日お話しするUIcollectionViewは、この3つのメリットを同時に体現しています.
    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){}
    }
    

    上は私が会社のために設計した2番目のバージョンで、swift 3.0とPOPの思想に合っています.このコードはもともと私が会社の設計テンプレートに貼ったものです.この簡単な多重finalクラスです.このクラスの役割は、プロジェクト全体のさまざまなUIcollectionViewを管理することです.
    基本的な考え方は、ViewModelの存在に合わせてcellのUIとUserInteractionをViewControlから分離し、上の工場類を通じて組み立てることです.これにより、各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) {
            //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が1つしかなく、IBActionがなくて、とてもいいです.今Cellの創立者として、私はこのcellに対応するView Modelが私の需要を処理することができることを保証する義務があります.それでは、私の需要はtitleLabelがStringタイプのデータを必要としていることです.だから、私はprotocolを発表して、私のviewmodelが必ず私にこのデータを提供することを保証します.また、このデータを入手した後、親のGeneric Methodを通じてUIの更新を実現しました.注意しなければならないのは、この親の方法は安全ではないので、Tには制限がないので、私たちは自分で安全性検査をする必要があります.
    次に、このcellに対してView Modelを設計する必要があります.
    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}
    }
    

    iOS 10以前はUIcollectionViewで自動sizeはサポートされていなかったため、このViewModelはViewの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]}
    }
    
    

    完全なプロジェクト:https://github.com/LHLL/MVVMSampleこのプロジェクトでは、UIButtonなどのIBActionを処理するためにViewモデルを使用する方法もdemoしました.