最近银行对项目提出了新的要求,80%的Unit Test成为了硬指标。特别是在丧失了Uber合作以后,为了尽快挽回名声,对技术人员提出了更高的要求。因为历史原因,项目有大量的C和OC混编的代码,而且长待七年的代码量不是一两天能够完成了。在长达一个月的扯皮以后,标准降低为了近一年以后的新代码必须达标。近一年的代码主要都是swift项目,虽然大部分都是MVC项目,但是改造成MVVM也并不是什么特别困难的事情。
个人对于MVVM一直相对比较抵触,因为我个人工作的内容有大量的prototype和POC,因此MVVM会显著增加代码量并且降低开发速度。而且我觉得网上很多人言MVVM必及automated UI是完全没有道理的。虽然reactiveCocoa之类的函数式编程是体现了MVVM的思想,但是MVVM和他们是不可以直接画上等号的。我个人对MVVM的理解在于,MVVM可以比较彻底的将UI和业务逻辑分离。UI和业务逻辑分离的好处是显而易见的:
- 对于可以复用的UI module, 能够轻松做到drag&drop;
- 对于业务逻辑的替换相对比较容易,比如从第三方网络库切换到自己的网络库;
- 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。