前言
在實(shí)踐 casa 的去Model化后飞袋,為了解決網(wǎng)絡(luò)數(shù)據(jù)獲取后如何處理、如何更新、如何監(jiān)聽(tīng)模型數(shù)據(jù)變化等一系列細(xì)節(jié)問(wèn)題杏瞻,而設(shè)計(jì)了AGViewModel。
其主要作用就是管理視圖與數(shù)據(jù)之間的關(guān)系衙荐,簡(jiǎn)化工作捞挥,盡量少做重復(fù)事情。
其實(shí)從減少工程的業(yè)務(wù)代碼量來(lái)說(shuō)是很可觀的忧吟,開(kāi)發(fā)效率提高不少砌函。但是對(duì)于這樣的設(shè)計(jì)會(huì)不會(huì)帶來(lái)測(cè)試上的問(wèn)題,或者一些我沒(méi)有注意到的架構(gòu)上的缺陷溜族?由于經(jīng)驗(yàn)有限讹俊,還希望發(fā)現(xiàn)的朋友批評(píng)指出。
AGViewModel 的設(shè)計(jì)思路
持有一個(gè)字典數(shù)據(jù)模型煌抒,并弱引用視圖仍劈。==> View <·· AGViewModel -> Model
- 用戶通過(guò)AGViewModel 對(duì)字典模型進(jìn)行數(shù)據(jù)增刪改查。
- 數(shù)據(jù)改變后寡壮,用戶可以通過(guò)AGViewModel 通知視圖更新自己的Size 或刷新UI界面贩疙。
- 具體怎樣計(jì)算Size 和怎樣顯示UI 由視圖自己決定。(當(dāng)需要這么做的時(shí)候况既,AGViewModel會(huì)把數(shù)據(jù)提供給視圖)
- 當(dāng)視圖產(chǎn)生了用戶的點(diǎn)擊事件時(shí)这溅,視圖通過(guò)AGViewModel 把事件傳遞給 ViewController 處理。
- 用戶對(duì)數(shù)據(jù)的操作和對(duì)事件的處理都是在 ViewController 上進(jìn)行的棒仍。(對(duì)悲靴,不要忘了 ViewController也要干活)
- 大體關(guān)系是:
- View-顯示UI,接收UI事件降狠;ViewModel-協(xié)調(diào)視圖和控制器干活对竣;Model-??;Controller-處理數(shù)據(jù)和UI事件榜配。
AGViewModel 對(duì)視圖Size 的管理方法否纬。
/** 獲取 bindingView 的 size,從緩存中取蛋褥。如果有“需要緩存的視圖Size”的標(biāo)記临燃,重新計(jì)算并緩存。*/
- (CGSize) ag_sizeOfBindingView;
/** 直接傳進(jìn)去計(jì)算并返回視圖Size烙心,如果有“需要緩存的視圖Size”的標(biāo)記膜廊,重新計(jì)算并緩存。*/
- (CGSize) ag_sizeForBindingView:(UIView<AGVMIncludable> *)bv;
/** 計(jì)算并緩存綁定視圖的Size */
- (CGSize) ag_cachedSizeByBindingView:(UIView<AGVMIncludable> *)bv;
/** 對(duì)“需要緩存的視圖Size”進(jìn)行標(biāo)記淫茵;當(dāng)調(diào)用獲取視圖Size的方法時(shí)爪瓜,從視圖中取。*/
- (void) ag_setNeedsCachedBindingViewSize;
/** 如果有“需要緩存的視圖Size”的標(biāo)記匙瘪,重新計(jì)算并緩存铆铆。*/
- (void) ag_cachedBindingViewSizeIfNeeded;
AGViewModel 對(duì)視圖UI 的管理方法。
/** 馬上更新數(shù)據(jù) 并 刷新視圖 */
- (void) ag_refreshUIByUpdateModelInBlock:(nullable NS_NOESCAPE AGVMUpdateModelBlock)block;
/** 更新數(shù)據(jù)丹喻,并對(duì)“需要刷新UI”進(jìn)行標(biāo)記薄货;當(dāng)調(diào)用ag_refreshUIIfNeeded時(shí),刷新UI界面碍论。*/
- (void) ag_setNeedsRefreshUIModelInBlock:(nullable NS_NOESCAPE AGVMUpdateModelBlock)block;
/** 對(duì)“需要刷新UI”進(jìn)行標(biāo)記谅猾;當(dāng)調(diào)用ag_refreshUIIfNeeded時(shí),刷新UI界面鳍悠。*/
- (void) ag_setNeedsRefreshUI;
/** 刷新UI界面税娜。*/
- (void) ag_refreshUI;
/** 如果有“需要刷新UI”的標(biāo)記,馬上刷新界面藏研。 */
- (void) ag_refreshUIIfNeeded;
視圖要做的兩件事
// 設(shè)置模型數(shù)據(jù)巧涧,更新UI
- (void)setViewModel:(AGViewModel *)viewModel
{
[super setViewModel:viewModel];
// TODO
}
// 計(jì)算返回自己的 Size
- (CGSize) ag_viewModel:(AGViewModel *)vm sizeForLayout:(UIScreen *)screen
{
// size
}
還能做什么?
把視圖的各種用戶操作傳遞給代理處理(一般都是Controller)
- 傳遞事件
- 參考Demo 中點(diǎn)擊書(shū)本封面
@protocol AGVMDelegate <NSObject>
/**
通過(guò) viewModel 的 @selector(ag_makeDelegateHandleAction:) 方法通知 delegate 做事遥倦。
通過(guò) viewModel 的 @selector(ag_makeDelegateHandleAction:info:) 方法通知 delegate 做事谤绳。
*/
@optional
- (void) ag_viewModel:(AGViewModel *)vm handleAction:(nullable SEL)action;
- (void) ag_viewModel:(AGViewModel *)vm handleAction:(nullable SEL)action info:(nullable AGViewModel *)info;
@end
作為本級(jí)控制器與下級(jí)控制器之間的橋梁
- 傳遞數(shù)據(jù)
- 參考Demo 中刪除書(shū)本
鍵值觀察 KVO
- 觀察內(nèi)部字典數(shù)據(jù)的變化
- 傳遞響應(yīng)事件
- 參考Demo 中刪除書(shū)本
數(shù)據(jù)歸檔
- 根據(jù)你需要提取的keys,把數(shù)據(jù)歸檔寫(xiě)入文件
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.tableViewManager.vmm];
[data writeToURL:[self _archiveURL] atomically:YES];
Json轉(zhuǎn)換
- 根據(jù)你需要提取的keys袒哥,拼接成Json字符串(后臺(tái)說(shuō):你直接扔個(gè)Json給我缩筛,接口寫(xiě)不動(dòng)了??)
- 根據(jù)Json轉(zhuǎn)成字典和數(shù)組(提供了C函數(shù))
// 取數(shù)據(jù)
NSData *data = [NSData dataWithContentsOfURL:[self _archiveURL]];
AGVMManager *vmm = [NSKeyedUnarchiver unarchiveObjectWithData:data];
// json
NSString *json = [vmm ag_toJSONString];
NSError *error;
NSDictionary *dict = ag_newNSDictionaryWithJSONString(json, &error);
對(duì)id類(lèi)型數(shù)據(jù)判斷處理
- 對(duì)API返回?cái)?shù)據(jù)的邊界判斷和處理
- 參考 AGVMSafeAccessible 協(xié)議
類(lèi)之間的關(guān)系
AGViewModel 與 AGVMPackagable協(xié)議 的關(guān)系
- 遵守AGVMPackagable協(xié)議的類(lèi)(簡(jiǎn)稱(chēng)VMP),提供了對(duì)API數(shù)據(jù)的處理堡称,并生成 AGViewModel瞎抛。
- 一個(gè)視圖是可以顯示多個(gè)API數(shù)據(jù)的,例如一個(gè)參與抽獎(jiǎng)的用戶和參與投票的用戶却紧,獲取數(shù)據(jù)的API不同桐臊,但顯示效果是一致的胎撤。
- 那么VMP就是為視圖服務(wù)的,把各種各樣的API數(shù)據(jù)處理成視圖可以接受的數(shù)據(jù)断凶。(視圖就是通過(guò)setViewModel:來(lái)更新UI界面的)
- 當(dāng)一個(gè)界面需要顯示各種Cell的時(shí)候伤提,只要把API數(shù)據(jù)經(jīng)過(guò)不同的VMP過(guò)濾就可以取出具備顯示條件的數(shù)據(jù)了。
- Cell 的復(fù)用很簡(jiǎn)單:
- 調(diào)用類(lèi)方法+ag_dequeueCellBy:for: 獲取
- 當(dāng)然前提是要在前面注冊(cè)+ag_registerCellBy:
AGVMPackagable協(xié)議
@protocol AGVMPackagable <NSObject>
- (AGViewModel *) ag_packageData:(NSDictionary *)dict forObject:(nullable id)obj;
@end
視圖復(fù)用的一些協(xié)議已經(jīng)在分類(lèi)中實(shí)現(xiàn)(有特殊需求 Override)
#pragma mark - ------------- ViewModel 相關(guān)協(xié)議 --------------
#pragma mark BaseReusable Protocol
@protocol AGBaseReusable <NSObject>
@required
+ (NSString *) ag_reuseIdentifier;
@optional
/** 如果使用默認(rèn) nib认烁、xib 創(chuàng)建視圖肿男,又需要打包成庫(kù)文件的時(shí)候,請(qǐng)返回你的資源文件目錄却嗡。*/
+ (NSBundle *) ag_resourceBundle;
@end
#pragma mark CollectionViewCell Protocol
@protocol AGCollectionCellReusable <AGBaseReusable>
@required
+ (void) ag_registerCellBy:(UICollectionView *)collectionView;
+ (__kindof UICollectionViewCell *) ag_dequeueCellBy:(UICollectionView *)collectionView
for:(nullable NSIndexPath *)indexPath;
@end
#pragma mark HeaderViewReusable Protocol
@protocol AGCollectionHeaderViewReusable <AGBaseReusable>
@required
+ (void) ag_registerHeaderViewBy:(UICollectionView *)collectionView;
+ (__kindof UICollectionReusableView *) ag_dequeueHeaderViewBy:(UICollectionView *)collectionView
for:(nullable NSIndexPath *)indexPath;
@end
#pragma mark FooterViewReusable Protocol
@protocol AGCollectionFooterViewReusable <AGBaseReusable>
@required
+ (void) ag_registerFooterViewBy:(UICollectionView *)collectionView;
+ (__kindof UICollectionReusableView *) ag_dequeueFooterViewBy:(UICollectionView *)collectionView
for:(nullable NSIndexPath *)indexPath;
@end
#pragma mark TableViewCell Protocol
@protocol AGTableCellReusable <AGBaseReusable>
@required
+ (void) ag_registerCellBy:(UITableView *)tableView;
+ (__kindof UITableViewCell *) ag_dequeueCellBy:(UITableView *)tableView
for:(nullable NSIndexPath *)indexPath;
@end
#pragma mark HeaderFooterViewReusable Protocol
@protocol AGTableHeaderFooterViewReusable <AGBaseReusable>
@required
+ (void) ag_registerHeaderFooterViewBy:(UITableView *)tableView;
+ (__kindof UITableViewHeaderFooterView *) ag_dequeueHeaderFooterViewBy:(UITableView *)tableView;
@end
AGViewModel 與 AGVMSection舶沛、AGVMManager 的關(guān)系
- 簡(jiǎn)單來(lái)說(shuō)就是 AGVMSection 管理一組 AGViewModel,AGVMManager 管理多組 AGViewModel(就是管理多個(gè)AGVMSection)窗价。
- 從視圖層面說(shuō)如庭,就是:
- AGViewModel 能夠管理 TableViewCell 的數(shù)據(jù)。
- AGVMSection 能夠管理 一組TableViewCell 和 HeaderView撼港、FooterView 的數(shù)據(jù)柱彻。
- AGVMManager 能夠管理 整個(gè)TableView 的數(shù)據(jù);
AGVMNotifier 與 AGViewModel 的關(guān)系
- 當(dāng)用戶需要監(jiān)聽(tīng) AGViewModel 的通用字典數(shù)據(jù)變化時(shí)餐胀,后臺(tái)操作就是由 AGVMNotifier 提供的哟楷。
-AGVMNotifier 是通過(guò)KVO 來(lái)處理監(jiān)聽(tīng)事件的。AGVMNotifier 同時(shí)也處理了KVO的崩潰問(wèn)題否灾。
AGVMPackager 與 AGViewModel 的關(guān)系
- AGVMPackager 就是方便打包AGViewModel 數(shù)據(jù)的單例卖擅。
- AGVMPackager 可以對(duì)數(shù)據(jù)進(jìn)行打印,打印出需要的代碼墨技。
打印需要的代碼
/**
分解打印 JSON 為常量惩阶。(嵌套支持)
@param object 待分解的字典或數(shù)組
@param moduleName 模塊的名稱(chēng)
*/
- (void) ag_resolveStaticKeyFromObject:(id)object
moduleName:(NSString *)moduleName;
Debug 查看內(nèi)部數(shù)據(jù)結(jié)構(gòu)
Cocoapods 集成
platform :ios, '7.0'
target 'AGViewModel' do
pod 'AGViewModel'
end