前言
在实践 casa 的去Model化后,为了解决网络数据获取后如何处理、如何更新、如何监听模型数据变化等一系列细节问题,而设计了AGViewModel。
其主要作用就是管理视图与数据之间的关系,简化工作,尽量少做重复事情。
其实从减少工程的业务代码量来说是很可观的,开发效率提高不少。但是对于这样的设计会不会带来测试上的问题,或者一些我没有注意到的架构上的缺陷?由于经验有限,还希望发现的朋友批评指出。
AGViewModel 的设计思路
持有一个字典数据模型,并弱引用视图。==> View <·· AGViewModel -> Model
- 用户通过AGViewModel 对字典模型进行数据增删改查。
- 数据改变后,用户可以通过AGViewModel 通知视图更新自己的Size 或刷新UI界面。
- 具体怎样计算Size 和怎样显示UI 由视图自己决定。(当需要这么做的时候,AGViewModel会把数据提供给视图)
- 当视图产生了用户的点击事件时,视图通过AGViewModel 把事件传递给 ViewController 处理。
- 用户对数据的操作和对事件的处理都是在 ViewController 上进行的。(对,不要忘了 ViewController也要干活)
- 大体关系是:
- View-显示UI,接收UI事件;ViewModel-协调视图和控制器干活;Model-🌚;Controller-处理数据和UI事件。
AGViewModel 对视图Size 的管理方法。
/** 获取 bindingView 的 size,从缓存中取。如果有“需要缓存的视图Size”的标记,重新计算并缓存。*/
- (CGSize) ag_sizeOfBindingView;
/** 直接传进去计算并返回视图Size,如果有“需要缓存的视图Size”的标记,重新计算并缓存。*/
- (CGSize) ag_sizeForBindingView:(UIView<AGVMIncludable> *)bv;
/** 计算并缓存绑定视图的Size */
- (CGSize) ag_cachedSizeByBindingView:(UIView<AGVMIncludable> *)bv;
/** 对“需要缓存的视图Size”进行标记;当调用获取视图Size的方法时,从视图中取。*/
- (void) ag_setNeedsCachedBindingViewSize;
/** 如果有“需要缓存的视图Size”的标记,重新计算并缓存。*/
- (void) ag_cachedBindingViewSizeIfNeeded;
AGViewModel 对视图UI 的管理方法。
/** 马上更新数据 并 刷新视图 */
- (void) ag_refreshUIByUpdateModelInBlock:(nullable NS_NOESCAPE AGVMUpdateModelBlock)block;
/** 更新数据,并对“需要刷新UI”进行标记;当调用ag_refreshUIIfNeeded时,刷新UI界面。*/
- (void) ag_setNeedsRefreshUIModelInBlock:(nullable NS_NOESCAPE AGVMUpdateModelBlock)block;
/** 对“需要刷新UI”进行标记;当调用ag_refreshUIIfNeeded时,刷新UI界面。*/
- (void) ag_setNeedsRefreshUI;
/** 刷新UI界面。*/
- (void) ag_refreshUI;
/** 如果有“需要刷新UI”的标记,马上刷新界面。 */
- (void) ag_refreshUIIfNeeded;
视图要做的两件事
// 设置模型数据,更新UI
- (void)setViewModel:(AGViewModel *)viewModel
{
[super setViewModel:viewModel];
// TODO
}
// 计算返回自己的 Size
- (CGSize) ag_viewModel:(AGViewModel *)vm sizeForLayout:(UIScreen *)screen
{
// size
}
还能做什么?
把视图的各种用户操作传递给代理处理(一般都是Controller)
- 传递事件
- 参考Demo 中点击书本封面
@protocol AGVMDelegate <NSObject>
/**
通过 viewModel 的 @selector(ag_makeDelegateHandleAction:) 方法通知 delegate 做事。
通过 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
作为本级控制器与下级控制器之间的桥梁
- 传递数据
- 参考Demo 中删除书本
键值观察 KVO
- 观察内部字典数据的变化
- 传递响应事件
- 参考Demo 中删除书本
数据归档
- 根据你需要提取的keys,把数据归档写入文件
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self.tableViewManager.vmm];
[data writeToURL:[self _archiveURL] atomically:YES];
Json转换
- 根据你需要提取的keys,拼接成Json字符串(后台说:你直接扔个Json给我,接口写不动了😅)
- 根据Json转成字典和数组(提供了C函数)
// 取数据
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);
对id类型数据判断处理
- 对API返回数据的边界判断和处理
- 参考 AGVMSafeAccessible 协议
类之间的关系
AGViewModel 与 AGVMPackagable协议 的关系
- 遵守AGVMPackagable协议的类(简称VMP),提供了对API数据的处理,并生成 AGViewModel。
- 一个视图是可以显示多个API数据的,例如一个参与抽奖的用户和参与投票的用户,获取数据的API不同,但显示效果是一致的。
- 那么VMP就是为视图服务的,把各种各样的API数据处理成视图可以接受的数据。(视图就是通过setViewModel:来更新UI界面的)
- 当一个界面需要显示各种Cell的时候,只要把API数据经过不同的VMP过滤就可以取出具备显示条件的数据了。
- Cell 的复用很简单:
- 调用类方法+ag_dequeueCellBy:for: 获取
- 当然前提是要在前面注册+ag_registerCellBy:
AGVMPackagable协议
@protocol AGVMPackagable <NSObject>
- (AGViewModel *) ag_packageData:(NSDictionary *)dict forObject:(nullable id)obj;
@end
视图复用的一些协议已经在分类中实现(有特殊需求 Override)
#pragma mark - ------------- ViewModel 相关协议 --------------
#pragma mark BaseReusable Protocol
@protocol AGBaseReusable <NSObject>
@required
+ (NSString *) ag_reuseIdentifier;
@optional
/** 如果使用默认 nib、xib 创建视图,又需要打包成库文件的时候,请返回你的资源文件目录。*/
+ (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 的关系
- 简单来说就是 AGVMSection 管理一组 AGViewModel,AGVMManager 管理多组 AGViewModel(就是管理多个AGVMSection)。
- 从视图层面说,就是:
- AGViewModel 能够管理 TableViewCell 的数据。
- AGVMSection 能够管理 一组TableViewCell 和 HeaderView、FooterView 的数据。
- AGVMManager 能够管理 整个TableView 的数据;
AGVMNotifier 与 AGViewModel 的关系
- 当用户需要监听 AGViewModel 的通用字典数据变化时,后台操作就是由 AGVMNotifier 提供的。
-AGVMNotifier 是通过KVO 来处理监听事件的。AGVMNotifier 同时也处理了KVO的崩溃问题。
AGVMPackager 与 AGViewModel 的关系
- AGVMPackager 就是方便打包AGViewModel 数据的单例。
- AGVMPackager 可以对数据进行打印,打印出需要的代码。
打印需要的代码
/**
分解打印 JSON 为常量。(嵌套支持)
@param object 待分解的字典或数组
@param moduleName 模块的名称
*/
- (void) ag_resolveStaticKeyFromObject:(id)object
moduleName:(NSString *)moduleName;
Debug 查看内部数据结构
Cocoapods 集成
platform :ios, '7.0'
target 'AGViewModel' do
pod 'AGViewModel'
end