MVVM初嘗試--UITableView資料Manager思路分享
本豺狼最近忙於新需求開發, 荒於研究, 心中倍感焦慮, 不過恰好專案中進行了一些新的嘗試, 自覺收穫頗豐, 趕緊著與諸位分享!
大體說下情況吧, 豺狼這期的需求中有一塊是修改詳情頁的模組順序及UI, 由於這個詳情頁是很老的程式碼了, 十多個模組並且基於UITableView開發的, 加之迭代中不斷新增刪除模組, 可想而知UITableView代理方法多麼的混亂和不堪入目, 邏輯死板, 牽一髮動全身, 總之非常糟糕. 正好藉著這期需求, 豺狼將整個詳情頁的業務邏輯梳理並重構! 好吧, 我們進入正題:
我是封面!
重構思路是什麼
重構首先一點就是要有一個整體的思路, 對於詳情頁而言, 由於不同模組之間差異性大, 模組種類多, 順序及數量改動多
UIScrollView
作為底層View來開發, 這就涉及到了模組之間高度的計算, 由於豺狼沒有親身實踐, 不敢斷言合理與否, 但自覺相對麻煩, 靈活性也差一些, 自然維護起來多少會出現問題, 所以豺狼還是繼承原有邏輯, 在UITableView
上進行模組的新增, 當然如果偏頗之處, 請諸位指點一二.底層View確定後就是具體的程式碼結構邏輯了, 既然詳情頁有諸多特點, 那麼我們如何考慮結構呢? 我的思路是使用一個
SectionTypeArray
來進行具體模組的管理, 建立一個列舉來體現出所有模組的種類, 這樣每次需求變更時, 只要修改相應的SectionTypeArray
typedef NS_ENUM(NSUInteger, ViewControllerSectionType) {
ViewControllerSectionTypeUser,
ViewControllerSectionTypeSport,
ViewControllerSectionTypeFavortiteFood,
};
- (NSArray *)sectionTypeArray {
if (_sectionTypeArray == nil) {
_sectionTypeArray = @[@(ViewControllerSectionTypeUser),
@(ViewControllerSectionTypeSport),
@(ViewControllerSectionTypeFavortiteFood)];
}
return _sectionTypeArray;
}
如何管理UITableView的資料
既然我們確定了模組管理策略, 那就要在這個策略基礎上構造程式碼, 首先就是UITableView
需要根據這個模組管理策略來靈活變動, 這樣的話我們代理方法中就不能把邏輯寫死, 給我啟發的是早前看過的一個Demo--SigmaTableViewModel.
我們只需要把UITableView
的所有資料都用一個模型儲存了起來, 具體呼叫的時候根據NSIndexPath
從陣列中取出來不就好了?
return Array[IndexPath.row]
多麼優雅的寫法! 於是乎問題迎刃而解.
不過我認為這個Demo中封裝的過於死板, 所以自己單獨封裝了一個簡易版的.
@interface HRTableViewCellModel : NSObject
@property (nonatomic, assign) CGFloat cellHeight;
@property (nonatomic, copy) UITableViewCell *(^configCellBlock)(NSIndexPath *indexPath, UITableView *tableView);
@property (nonatomic, copy) void (^selectCellBlock)(NSIndexPath *indexPath, UITableView *tableView);
/*
其他屬性按需求新增
*/
@end
@interface HRTableViewModel : NSObject
@property (nonatomic, strong) NSMutableArray <HRTableViewCellModel *> *cellModelArray;
@property (nonatomic, assign) CGFloat headerHeight;
@property (nonatomic, assign) CGFloat footerHeight;
@property (nonatomic, strong) NSString *headerTitle;
@property (nonatomic, strong) NSString *footerTitle;
@property (nonatomic, strong) UIView *headerView;
@property (nonatomic, strong) UIView *footerView;
@property (nonatomic, copy) UIView * (^headerViewBlock)(NSInteger section, UITableView *tableView);
@property (nonatomic, copy) UIView * (^footerViewBlock)(NSInteger section, UITableView *tableView);
@end
所有的資料都用這個模型儲存起來, 用的時候直接讀取模型即可, 任你是順著排列倒著排列還是跳著排列隨便你, 想想就激動~
這個模型是用block
來傳遞具體的業務邏輯程式碼塊, 因為是簡易版有些功能可以自己拓展, 需要提到的一點就是對於使用block
造成的迴圈引用一定要注意!
靈活的詳情頁
先來展示下詳情頁有多麼靈活
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.sectionModelArray.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
HRTableViewModel *model = self.sectionModelArray[section];
return model.cellModelArray.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
HRTableViewModel *model = self.sectionModelArray[indexPath.section];
HRTableViewCellModel *cellModel = model.cellModelArray[indexPath.row];
return cellModel.cellHeight;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 40;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
HRTableViewModel *model = self.sectionModelArray[indexPath.section];
HRTableViewCellModel *cellModel = model.cellModelArray[indexPath.row];
return cellModel.configCellBlock(indexPath, tableView);
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
HRTableViewModel *model = self.sectionModelArray[indexPath.section];
HRTableViewCellModel *cellModel = model.cellModelArray[indexPath.row];
cellModel.selectCellBlock(indexPath, tableView);
}
就是這樣, 沒有一行具體的業務邏輯在這裡, 所有的相關業務都在外面實現好, 代理方法直接呼叫即可.
簡單說明一下
首先我們需要一個SectionModelArray
來儲存UITableView的資料, 然後我們根據SectionTypeArray
中的模組型別, 用switch
來塞入資料.
+ (NSArray <HRTableViewCellModel *> *)reformDataToSectionModelArray:(id)data delegate:(id)delegate {
ViewControllerDataSource *dataSource = [[ViewControllerDataSource alloc] init];
dataSource.delegate = delegate;
[dataSource.sectionModelArray removeAllObjects];
dataSource.dataDic = (NSDictionary *)data;
for (NSNumber *type in dataSource.sectionTypeArray) {
switch (type.integerValue) {
case ViewControllerSectionTypeUser: {
[dataSource configUserCellModel];
}
break;
case ViewControllerSectionTypeSport: {
[dataSource configSportCellModel];
}
break;
case ViewControllerSectionTypeFavortiteFood: {
[dataSource configFavoriteFoodCellModel];
}
break;
default:
break;
}
}
return [dataSource.sectionModelArray copy];
}
// 舉例: 塞入UserCell的資料
- (void)configUserCellModel {
// section的資料
HRTableViewModel *sectionModel = [[HRTableViewModel alloc] init];
[sectionModel setHeaderTitle:@"使用者資訊"];
// 塞入SectionModelArray
[self.sectionModelArray addObject:sectionModel];
// 具體cell的資料
HRTableViewCellModel *cellModel = [[HRTableViewCellModel alloc] init];
[cellModel setConfigCellBlock:^UITableViewCell *(NSIndexPath *indexPath, UITableView *tableView) {
__weak typeof(&*self) weakSelf = self;
UserCell *cell = (UserCell *)[weakSelf configUserCell:tableView indexPath:indexPath];
return cell;
}];
[cellModel setSelectCellBlock:^(NSIndexPath *indexPath, UITableView *tableView) {
__weak typeof(&*self) weakSelf = self;
ViewController *vc = [[ViewController alloc] init];
[((ViewController *)weakSelf.delegate).navigationController pushViewController:vc animated:YES];
}];
// 塞入section的cellModelArray中
[sectionModel.cellModelArray addObject:cellModel];
}
至此一個模組的邏輯就新增好了, 其他模組類似的邏輯一個個新增進去.
另外對於cell的動態高度, 如果是model
中計算高度, 直接寫就可以, 如果是cell
中利用控制元件計算高度, 就要在configCell方法中重新從陣列中取出對應CellModel傳入, 並且實現- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
方法. 再次強調切忌迴圈引用!
關於MVVM的設計模式感想
對於看著逼格很高的MVVM, 豺狼其實瞭解的並不多, 但是借用田神Cosa Yaloyum部落格中的一張圖可以更好地說明,
部落格在此, 乾貨很多.
http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/
豺狼對此理解是所謂MVVM模式就是在MVC下不斷對其Controller
瘦身而形成具體的處理弱業務的ViewModel
, 我覺得更應該叫做"M-VM-C-V".
隨著專案不斷迭代更新, Controller
中承載了很多不能在View
和Model
中寫的弱業務程式碼, 造成其體積越來越大, 維護越發困難, 其中就包括UITableView
的datasource
和delegate
的代理用到的邏輯, 所以豺狼嘗試著在這個Demo的基礎上分離這些弱業務邏輯, 對Controller
進行瘦身!
新的結構圖
於是豺狼專門分離出一個用來管理UITableView資料的類ViewControllerDataSource
, 按照我的理解, 這裡的ViewControllerDataSource
就是MVVM中的ViewModel
, 負責處理UITableView
中涉及到的弱業務(所謂弱業務與強業務也是從田神那偷來的概念~).
// ViewControllerDataSource.h
@interface ViewControllerDataSource : NSObject
+ (NSArray <HRTableViewModel *> *)reformDataToSectionModelArray:(id)data delegate:(id)delegate;
@end
傳入資料->加工成能用的資料模型->具體展示, Controller
在裡面只起到了邏輯分發轉接的作用, 程式碼量大大減少, 這樣就可以集中處理強業務邏輯, 也許這個Demo中看的並不明顯, 但豺狼在專案中實踐下來, 節省了2000行程式碼....而且整體邏輯更加清晰, 各司其職.ViewControllerDataSource
作為一個Manager就是處理UITableView
相關.
豺狼認為以後的Controller
可以用這種思路進一步拆分細化, 比如網路請求邏輯可以單獨建一個Manager管理, 唐巧的YTKNetwork就是這麼做的, 這樣一定程度上也可以起到解耦的作用.
最後
重劍無鋒,大巧不工。 ---- 《神鵰俠侶》
以這句話作為結束語, MVVM作為新的設計模式並不是死板固定的, 更多的還是根據需求進行使用, 簡單的頁面MVC, 複雜的頁面進行拆分, 不拘泥於MVVM的格式, 才是最正確的用法.
最後再次強調田神的高質量部落格, 關於架構的部分每一篇都能讀一天~如果諸位能看到這裡,希望可以給豺狼點個贊鼓勵一下!
文/赤脊山的豺狼人(簡書作者)
原文連結:http://www.jianshu.com/p/a31d2b606e94
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。