今天寫這篇文章的目的,是提供一種思路,來幫助大家解決控制器非常臃腫的問題,對控制器瘦身。
如果手邊有項目,不妨打開工程看一下你的控制器代碼有多少行,是不是非常多?再看一下tableView的代理方法cellForRow和heightForRow的代碼是不是也是非常多?裡面夾雜著switch和大量if esle的判斷邏輯的代碼。後期維護看著這些if else是不是特別煩躁?特別是自己在維護前人寫的代碼,並且還沒有注釋 一團糟,是不是有更想罵人的沖動?別怕,這裡給您提供一種解決思路,讓你的tableView代理方法再也沒有這種讓人頭疼的if else判斷邏輯,讓你的控制器代碼量大大減少,並且後期維護成本也大大的減少。
在說具體解決思路前,先給大家簡單復習一下MVC和MVVM,因為今天的主題也是和MVVM有關系。MVC模式大家都很熟悉了,就是Model,View,Controller三層,Model負責數據層,Controller負責業務邏輯層,View負責界面顯示層,Model和View通過Controller來實現橋接交互,程序的擴展性很好,好處多多。但是呢,MVC也有它自身的缺陷,那就是控制器太臃腫,如果你想在控制器中定位某一個點是比較麻煩的事。
那麼為什麼控制器如此龐大,就是因為tableView的代理方法裡的cell 判斷邏輯全在控制器,以及網絡請求也在控制器發起,另外還有一些其他的業務邏輯。有沒有什麼更高的模式呢?MVVM模式就是MVC模式的升級版。
MVVM中Model依然負責數據層,Controller單單負責View的展示和更新,其他業務邏輯不管。View依然負責界面顯示。那麼ViewController之前負責的業務邏輯現在誰來負責呢?我們再新建一個ViewModel層,處在ViewController層和Model層之間,專門負責業務邏輯,以及網絡請求等任務。ViewController從ViewModel中獲取數據然後顯示在View上,它並不和Model層直接打交道,和Model層直接打交道的是ViewModel層 。
下面附上一張經典gif圖片,幫助大家理解兩者之間的關系。
好了,前面的都是回顧一些相關知識,為理解接下來的內容做基礎,如果想要深入了解MVC,MVVM可以網上找下,這類文章很多。
大家可能疑惑到底什麼是面向超類編程,其實就是圍繞繼承這個特性,子類cell繼承父類cell,面向父類這個對象來編程,最終對控制器的tableView進行瘦身,也不止是對tableView優化,配合MVVM新建ViewModel可以抽離很大一部分控制器的代碼。現在還不清楚沒關系,下面會有很詳細的描述讓你明白。^_^ 我下面先把大家常用的控制器tableView代理方法的寫法,給黏貼出來,然後再用新的面向超類的寫法給黏貼出來,大家就可以明顯體會到使用面向超類寫法的好處了。
老方式大眾寫法
面向超類編程寫法
看到這裡,可能會有人吐槽了,新寫法就比老式寫法少了十幾行嘛?其實不是這樣的,首先這個demo我只寫了三個cell作為例子,真實的項目極少一個控制器只有三個cell吧?控制器的cell越多,好處越明顯, 因為在後期不管添加多少cell,控制器tableView的代理方法中的代碼幾乎都不會增加,相當於構建了一個模版,只需要在新添加cell的內部配置即可。其次,真實的項目也不可能業務邏輯這麼簡單吧?肯定在if else中嵌套了很多其他的邏輯代碼,致使tableView看起來很臃腫。
面向超類編程的好處:
1.控制器瘦身。[控制器內部代碼量大幅度減少,邏輯更加清晰]
2.後期維護成本大大降低。[後期如果想添加或者刪除cell,只需要新建或者刪除一個子類cell,在viewModel中添加或刪除一個identifier即可,控制器幾乎不用加任何代碼]
面向超類的壞處:
1.新建更多的cell文件和一個viewModel文件,包大小會響應增加。
下面就具體講解面向超類編程瘦身大概要做什麼:
一,新建一個繼承自UITableViewCell的父類cell
#import#import "ResponseNewProgrammeData.h" #import "NewProgrammeCellHeightProtocol.h" //子類需要有回調事件的代理 @protocol NewProgrammeTableViewCellProtocol - (void)cell1DidSelectedRightButton; - (void)cell2DidSelectedRightButton; - (void)cell3DidSelectedRightButton; @end @interface NewProgrammeBaseCell : UITableViewCell @property (nonatomic, weak) id delegate; @property (nonatomic, strong) ResponseNewProgrammeData * responseNewProgrammeData; @end
首先,要包含控制器的數據源,因為子類cell的UI等操作全靠這個父類的數據源。
其次,要實現NewProgrammeCellHeightProtocol協議,作為計算高度用,具體用法在第二點講解。
最後,如果子類cell有點擊事件需要回調操作的,可再寫一個協議NewProgrammeTableViewCellProtocol作為屬性持有,在控制器中將delegate指向控制器作為回調使用。
二,新建一個NewProgrammeCellHeightProtocol
#import(BOOL)isStaticCell方法是在子類中使用的,如果當前cell是高度固定的靜態cell,就在返回YES,並且在cellHeight方法中返回固定高度。否則返回NO即可,也不需要寫+ (float)cellHeight方法。這兩個方法會在控制器的heightForRow方法中使用,計算當前cell高度。//針對cell的高度寫的協議 @protocol NewProgrammeCellHeightProtocol @optional + (BOOL)isStaticCell; + (float)cellHeight; @end
三,新建控制器所需要的所有cell,且繼承自剛才的父類cell
#import "NewProgrammeCell1.h" @interface NewProgrammeCell1 () @property (nonatomic, weak) IBOutlet UILabel *lblName; @end @implementation NewProgrammeCell1 @synthesize responseNewProgrammeData = _responseNewProgrammeData; - (void)setResponseNewProgrammeData:(ResponseNewProgrammeData *)responseNewProgrammeData { _responseNewProgrammeData = responseNewProgrammeData; self.lblName.text = _responseNewProgrammeData.string1; } + (BOOL)isStaticCell { return YES; } + (float)cellHeight { return 44; } - (IBAction)didPressedPush:(id)sender { if (self.delegate && [self.delegate respondsToSelector:@selector(cell1DidSelectedRightButton)]) { [self.delegate cell1DidSelectedRightButton]; } } @end
這個cell的高度是固定靜態的,所以isStaticCell方法返回YES,cellHeight返回高度。
這個cell中可以拿到控制器數據源,根據自己的需要去獲取數據。
這個cell的didPressedPush方法是模擬需要點擊事件的,回調給控制器。
其他cell的配置都大致是這樣的。
四,新建viewModel文件
#import "NewProgrammeViewModel.h" static NSString * const NewProgrammeCell1Identifier = @"NewProgrammeCell1"; static NSString * const NewProgrammeCell2Identifier = @"NewProgrammeCell2"; static NSString * const NewProgrammeCell3Identifier = @"NewProgrammeCell3"; @implementation NewProgrammeViewModel - (NSArray *)getIdentifierList { return @[NewProgrammeCell1Identifier, NewProgrammeCell2Identifier, NewProgrammeCell3Identifier]; } - (void)requestData { self.responseNewProgrammeData = [[ResponseNewProgrammeData alloc] init]; } @end
viewModel負責配置控制器所需要注冊的cell以及真正要顯示的cell,getIdentifierList返回需要注冊的所有cell。因為某些頁面的cell不是固定顯示的,可能根據數據源動態的來配置。同時,viewModel也負責網絡請求數據解析等其他業務邏輯代碼。這裡的viewModel相當於MVVM模式中的胖Model,不僅處理網絡請求,還處理頁面UI的配置等其他業務邏輯,這樣就不會使控制器那麼臃腫。
五,在控制器中做相應代碼配置
- (void)configTableViewCell { for (NSString * identifer in [self.viewModel getIdentifierList]) { [self.tableView registerNib:[UINib nibWithNibName:identifer bundle:nil] forCellReuseIdentifier:identifer]; } } #pragma mark - UITableViewDelegate - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.viewModel.getIdentifierList.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString * cellIdentifier = [self.viewModel.getIdentifierList objectAtIndex:indexPath.row]; NewProgrammeBaseCell * cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; cell.delegate = self; cell.responseNewProgrammeData = self.viewModel.responseNewProgrammeData; return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { NSString * cellIdentifier = [self.viewModel.getIdentifierList objectAtIndex:indexPath.row]; ClasscellClass = NSClassFromString(cellIdentifier); CGFloat height = 0; if ([cellClass isStaticCell]) { height = [cellClass cellHeight]; return height; } else { NewProgrammeBaseCell * cell = (NewProgrammeBaseCell*)[self tableView:tableView cellForRowAtIndexPath:indexPath]; height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingExpandedSize].height; return height; } }
首先,要實例化viewModel對象,在configTableViewCell方法中獲取需要注冊的所有cell,遍歷注冊。
其次,在cellForRow代理方法中,從viewModel對象中獲取所有注冊的cell的identifier,然後從tableView中獲取賦值給父類cell。再針對父類cell做一些賦值操作,也就是分別調用了子類cell,充分利用多態的特性。
最後,在heightForRow代理方法中,仍然是根據viewModel對象獲取所有注冊的cell的identifier,再根據identifier反射成對象子類cell的類對象,接著調用cell中的協議方法,來計算高度。
到這裡,面向超類編程瘦身基本思路都說完了