XZMocoa
示例项目
要运行示例工程,请在拉取代码后,先在Pods目录下执行pod install命令。
To run the example project, clone the repo, and run pod install from the Pods directory first.
版本需求
iOS 11.0+,Xcode 14.0+
如何安装
推荐使用 CocoaPods 安装 XZMocoa 框架,在Podfile文件中添加下面这行代码即可。
XZMocoa is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'XZMocoa'如何使用
UITableView是 iOS 开发中的常用组件,下面以编写UITableView列表为例,介绍如何使用 Mocoa 进行开发。
由于UITableView在设计上,并不是十分的符合在 MVVM 设计模式下使用,因此 Mocoa 将其简单地封装为XZMocoaTableView。
封装只是简单地将其放在UIView中,没有其它处理,在本质上与UITableView没有区别。
1、设计数据
将数据设计成符合UITableView两层数据结构的形式,肯定会大大的简化数据处理的过程,但实际开发过程中,肯定有各种各样的数据格式。
因此 Mocoa 为UITableView的数据设计了XZMocoaTableModel和XZMocoaTableViewSectionModel数据标准协议,以增加数据的通用性。
@protocol XZMocoaTableModel <XZMocoaModel>
@property (nonatomic, readonly) NSInteger numberOfSectionModels;
- (nullable id<XZMocoaTableViewSectionModel>)modelForSectionAtIndex:(NSInteger)index;
@end
@protocol XZMocoaTableViewSectionModel <XZMocoaModel>
@optional
@property (nonatomic, readonly) NSInteger numberOfCellModels;
- (nullable id)modelForCellAtIndex:(NSInteger)index;
- (NSInteger)numberOfModelsForSupplementaryKind:(XZMocoaKind)kind;
- (nullable id)modelForSupplementaryKind:(XZMocoaKind)kind atIndex:(NSInteger)index;
@end严格来讲,数据不应该承担业务逻辑,但是很明显,这两个协议只是为了统一获取UITableView列表数据的接口,可以算也可以不算是业务逻辑,
而将数据的标准,由数据自身处理,维护起来也更方便。
另外,Mocoa 会自动使用数组NSArray的中元素,而非数组本身;如果是二维数组,一维元素作为section数据,二维元素作为cell数据。
2、创建列表
// model, replace it with real data
NSArray *dataArray;
// viewModel
XZMocoaTableViewModel *tableViewModel = [[XZMocoaTableViewModel alloc] initWithModel:dataArray];
tableViewModel.module = XZMocoa(@"https://mocoa.xezun.com/table/");
[tableViewModel ready];
// view
XZMocoaTableView *tableView = [[XZMocoaTableView alloc] initWithFrame:self.view.bounds style:(UITableViewStyleGrouped)];
tableView.viewModel = tableViewModel;
[self.view addSubview:tableView];现在,你就可以运行代码,渲染列表了,虽然我们并没有创建cell,但是在 DEBUG 环境下,Mocoa 会使用“占位视图”渲染目标cell。
“占位视图”不能可以帮我们提前验证数据格式是否设计正确,还可以帮我们防止UITableView数据源带来的各种crash问题。
3、开发cell模块
使用 Mocoa 你可以将每一个cell都看作是完全独立的模块进行开发,然后注册到需要展示的tableView模块中即可。
开发cell模块,与开发普通 MVVM 模块的过程基本一样,仅需要按照 MVVM 的基本要求编写即可。
3.1 定义 View、ViewModel、Model
@interface ExampleCell : UITableViewCell <XZMocoaTableViewCell>
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@end
@interface ExampleCellViewModel : XZMocoaTableViewCellViewModel
@property (nonatomic, copy) NSString *name;
@end
@interface ExampleCellModel : NSObject <XZMocoaTableViewCellModel>
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@end除了ViewModel需要使用 Mocoa 提供的基类外,View和Model是完全自由的,协议XZMocoaTableViewCell和XZMocoaTableViewCellModel提供了默认实现,可以直接使用。
3.2 处理数据
ViewModel将数据处理为View展示所需的类型。
@implementation ExampleCellViewModel
- (void)prepare {
[super prepare];
self.height = 44.0;
ExampleModel *data = self.model;
self.name = [NSString stringWithFormat:@"%@ %@", data.firstName, data.lastName];
}
- (void)tableView:(XZMocoaTableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
/// 处理 cell 的点击事件
}
@end3.3 渲染视图
View根据ViewModel提供的数据进行展示。
@implementation ExampleCell
- (void)viewModelDidChange {
ExampleViewModel *viewModel = self.viewModel;
self.nameLabel.text = viewModel.name;
}
@end方法viewModelDidChange是 Mocoa 提供的方法,一般在这里装载视图内容。
3.4 注册模块
虽然section是逻辑层,在UITableView中虽然没有直接视图,但是 Mocoa 保留了它,因此cell是注册在section之下的。
@implementation ExampleCellModel
+ (void)load {
XZMocoa(@"https://mocoa.xezun.com/table/").section.cell.modelClass = self;
}
@end
@implementation ExampleCell
+ (void)load {
XZMocoa(@"https://mocoa.xezun.com/table/").section.cell.viewNibClass = self;
}
@end
@implementation ExampleCellViewModel
+ (void)load {
XZMocoa(@"https://mocoa.xezun.com/table/").section.cell.viewModelClass = self;
}
@end至此,使用XZMocoaTableView渲染列表的一个简单示例就完成了,再运行代码,就可以看到效果。
在这个示例中,我们只有一种类型的section和cell,不需要具名,所以直接使用.section.cell注册,更多详细用法,可参考“Example”示例工程。
使用 Mocoa 渲染列表,与使用原生的UITableView相比:
- 不用编写
delegate或dataSource方法。 - 不用先编写
cell,Mocoa 会先用占位视图替代,直到cell模块编写完成。 cell模块完全独立,编写cell后,仅需注册模块,不需在tableView或collectionView中注册。
还有,我们再也不用一遍遍地触发UITableView的Crash去调试数据、列表、cell的连通性了。
模块化
不论采用何种设计模式,都应该让你的代码模块化。这样在更新维护时,变动就可以控制在模块内,从而避免牵一发而动全身。
Mocoa 使用 MVVM 设计模式进行模块化,因为在 MVVM 设计模式下,视图可以通过自身的ViewModel管理逻辑,可以避免控制器变得越来越重。
Mocoa 为模块提供了基于 URL 的模块管理方案 XZMocoaDomain,任何模块都可以通过URL在XZMocoaDomain中注册。
[[XZMocoaDomain doaminForName:@"mocoa.xezun.com"] setModule:yourModule forPath:@"your/module/path"];上面例子中的模块地址为https://mocoa.xezun.com/your/module/path/,其中 URL 的scheme是任意的。
id yourModule = [[XZMocoaDomain doaminForName:@"mocoa.xezun.com"] moduleForPath:@"your/module/path"];XZMocoaDomain其实就是简单地使用NSMutableDictionary进行管理模块,所以你不必担心它的性能问题。
在实际开发中,有些提供了各种各样方法的“模块”,通过上面注册的方式拿到一个匿名的id类型,似乎显得多次一举。
但是在 Mocoa 看来,这样的“模块”并不是真正的模块,而只是一个组件或提供方法的工具类,因为真正的模块应该是能独自完成功能的,不需要或者仅需要少量简单明了的参数。
比如每个 App 实际上就是一个独立的模块,main(int, char *)是它们统一入口函数。
Mocoa 将每一个 MVVM 单元Model-View-ViewModel都视为一个 Mocoa 模块,即XZMocoaModule模块,并做如下约定。
Model使用-init作为初始化方法,或者开发者自行约定统一的方法。ViewModel使用-initWithModel:作为初始化方法。View中的UIViewController使用-initWithNibName:bundle:作为初始化方法View中的UIView一般使用-initWithFrame:作为初始化方法,像UITableViewCell等被管理的视图,则它们自身决定。
然后,我们就可以在 Mocoa 中注册 MVVM 模块的View、Model、ViewModel三个部分了。
XZMocoa(@"https://mocoa.xezun.com/").modelClass = Model.class;
XZMocoa(@"https://mocoa.xezun.com/").viewClass = View.class;
XZMocoa(@"https://mocoa.xezun.com/").viewModelClass = ViewModel.class;注:函数XZMocoa(url)是+[XZMocoaModule moduleForURL:]的便利写法。
MVVM 模块在注册后,我们就可以按照约定好的基本规则使用它们了,比如对于一个普通的视图模块,我们在拿到数据后,可以像下面这样使用它。
NSDictionary *data;
XZMocoaModule *module = XZMocoa(@"https://mocoa.xezun.com/");
id<XZMocoaModel> model = [module.modelClass yy_modelWithDictionary:data]; // 这里使用了 YYModel 组件
XZMocoaViewModel * viewModel = [[module.viewModelClass alloc] initWithModel:model];
UIView<XZMocoaView> * view = [[module.viewClass alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[self.view addSubview:view];对于页面UIViewController模块,Mocoa 认为它是一个独立模块,所以在启动页面时,提供了便利方法。
UIView<XZMocoaView> *view;
NSURL *url = [NSURL URLWithString:@"https://mocoa.xezun.com/main"];
[view.navigationController pushViewControllerWithMocoaURL:url animated:YES];即通过 URL 直接打开目的页面。使用View打开控制器,在 MVC 设计模式中是不合理的,但是在 MVVM 设计模式,UIViewController仅仅是特殊的View而已。
最后,注册模块的时机,要在所有业务逻辑开始之前,因此+load方法是最合适的。
+ (void)load {
XZMocoa(@"https://mocoa.xezun.com/examples/20/content/").viewNibClass = self;
}但是如果项目组对+load方法使用有限制,可以通过XZMocoaDomainModuleProvider协议自定义XZMocoaDomain的模块提供方式,比如读配置文件。
@protocol XZMocoaDomainModuleProvider <NSObject>
- (nullable id)domain:(XZMocoaDomain *)domain moduleForName:(NSString *)name atPath:(NSString *)path;
@end在层级关系中,子模块的路径,一般就是它的名字,比如:
https://mocoa.xezun.com/table/table模块https://mocoa.xezun.com/table/name1/name1是table模块的子模块https://mocoa.xezun.com/table/name1/name2/name2是name1模块的子模块,name1是table模块的子模块
如果子模块有分类,使用:分隔,比如:
https://mocoa.xezun.com/table/section/header:name1/name1是section模块的header子模块https://mocoa.xezun.com/table/section/footer:name2/name2是section模块的footer子模块
模块也可以没有名字和分类,但是在路径中,没有分类可以省略:,没有名字不能省略:,比如:
https://mocoa.xezun.com/table/name/合法https://mocoa.xezun.com/table/kind:name/合法https://mocoa.xezun.com/table/kind:/合法https://mocoa.xezun.com/table/:/合法https://mocoa.xezun.com/table/kind/不合法
Mocoa MVVM
Mocoa 建议使用 MVVM 模式设计您的代码,包括控制器,而且列表页面中,每一个区块视图cell也应该设计为独立的 MVVM 模块。
Mocoa 为更好的在页面中使用 MVVM 设计模式,拓展了一些原生能力。
XZMocoaView协议,Model 遵循此协议,以表明 Model 是 MVVM 中的Model元素。XZMocoaModel协议,View 遵循此协议,以表明 View 是 MVVM 中的View元素。XZMocoaViewModel基类,ViewModel提供的功能要复杂的多,无法通过协议的方式呈现,因此提供了基类。
Mocoa 与其说是框架,不如说是规范,通过协议规范 MVVM 的实现方法。
层级机制
在页面模块中,子视图模块,与父视图模块或控制器模块,存在明显的上下级关系。充分利用这种层级关系,可以更方便的处理页面中的一些上下级的交互逻辑,因此 Mocoa 设计了ViewModel的层级关系。
[superViewModel addSubViewModel:viewModel];
[viewModel insertSubViewModel:viewModel atIndex:1]然后我们就可以通过层级关系,收发emit事件。
// send the emition
- (void)emit:(NSString *)name value:(id)value;
// handle the emition
- (void)subViewModel:(XZMocoaViewModel *)subViewModel didEmit:(XZMocoaEmition *)emition;比如在UITableView列表中,cell模块改变了内容时,希望UITableView模块刷新页面时,可以像下面这样处理。
// 在 cell 中
- (void)handleUserAction {
// change the data then
self.height = 100; // a new height
[self emit:XZMocoaEmitUpdate value:nil];
}
// 在 UITableView 模块中
- (void)subViewModel:(__kindof XZMocoaViewModel *)subViewModel didEmit:(XZMocoaEmition *)emition {
if ([emition.name isEqualToString:XZMocoaEmitUpdate]) {
[self reloadData];
}
}当前这么做,需要一些默认的约定,比如将XZMocoaEmitUpdate作为刷新视图的事件。
在 MVC 中,解决上面的问题,一般是通过delegate实现,这明显或破坏模块的整体性,上层模块与下层模块的delegate形成了耦合,但是利用层级关系处理,就能很好的避免这一点。
同时,层级关系事件的局限性也很明显,仅适合处理比较明确的事件,不过在模块封装完整的情况下,下层模块也不应该有其它事件需要传递给上级处理。
ready 机制
在模块层级关系中,模块在创建时,可能并不需要立即初始化,或者模块需要额外的初始化参数,比如在UIViewController中,应该在viewDidLoad时初始化,因此 Mocoa 设计了ready机制来延迟ViewModel的初始化时机。
在ready机制下,开发者应该在ViewModel的-prepare方法中进行初始化。
- (void)prepare {
[super prepare];
// 执行当前模块的初始化
}
如果是顶层模块,应该在合适的时机调用ViewModel的-ready方法。比如页面模块,一般是顶层模块,建议在-viewDidLoad中执行。
- (void)viewDidLoad {
[super viewDidLoad];
Example20ViewModel *viewModel = [[Example20ViewModel alloc] initWithModel:nil];
[viewModel ready];
self.viewModel = viewModel;
self.tableView.viewModel = viewModel.tableViewModel;
}因为控制器顶层模块,引用模块时不需要准备数据,它的数据是ViewModel自行处理的,所以初始化它的model是nil,在View中自己创建ViewModel也是合理的。
同时 Mocoa 也约定:
- 在顶层独立的
UIViewController页面模块中,应由View(即UIViewController)在合适的时机自行创建ViewModel。
由外部提供数据的不完全独立的页面模块,加载使用方式则与UIView基本一致。
XZMocoaModule *module = XZMocoa(@"https://mocoa.xezun.com/");
id model;
XZMocoaViewModel *viewModel = [[module.viewModelClass alloc] initWithModel:model];
UIViewController<XZMocoaView> *nextVC = [module instantiateViewControllerWithOptions:nil];
nextVC.viewModel = viewModel; // not ready here, and nextVC must call -ready in -viewDidLoad method before use it.
[view.navigationController pushViewController:nextVC animated:YES];Mocoa 为独立的顶层模块,提供了进入的便利方法。
// UIViewController
- (void)presentViewControllerWithMocoaURL:(nullable NSURL *)url animated:(BOOL)flag completion:(void (^_Nullable)(void))completion;
- (void)addChildViewControllerWithMocoaURL:(nullable NSURL *)url;
// UINavigationController
- (void)pushViewControllerWithMocoaURL:(nullable NSURL *)url animated:(BOOL)animated;target-action
在 MVVM 设计模式中,View通过监听ViewModel的属性来展示页面,但是实际上,大部分情况下,View并不需要一直监听,因为大多数的View只需要渲染一次。
所以 Mocoa 没有设计如何实现监听的代码,因为大部分页面渲染在viewModelDidChange中就能完成了。
在剩下的小部分情况中,我们可以通过delegate的方式来实现,这比监听更直观,且易维护。
不过,使用delegate由于需要定义协议,使用起来比较麻烦,所以了简化这些在少量事件的处理,Mocoa 设计了target-action机制。
这是一种半自动的机制,使用NSString作为keyEvents,View在绑定的keyEvents之后,ViewModel在调用-sendActionsForKeyEvents:方法时,View绑定的方法就会被触发。
// view 监听了 viewModel 的 isHeaderRefreshing 属性
[viewModel addTarget:self action:@selector(headerRefreshingChanged:) forKeyEvents:@"isHeaderRefreshing"];
- (void)headerRefreshingChanged:(Example20ViewModel *)viewModel {
if (viewModel.isHeaderRefreshing) {
[self.tableView.contentView.xz_headerRefreshingView beginAnimating];
} else {
[self.tableView.contentView.xz_headerRefreshingView endAnimating];
}
}
// viewModel 发送事件
[self sendActionsForKeyEvents:@"isHeaderRefreshing"];target-action机制,相当于使用keysEvents代替了delegate协议,处理一些简单的事件。
MVVM 化适配
原生的大部分视图控件,在 MVVM 设计模式下使用,都是合适的,但某些特殊类型的视图,需要进行 MVVM 化之后,才适合在 MVVM 中使用。
比如具有视图管理功能的UITableView和UICollectionView列表视图,Mocoa 将它们封装为更适合在 MVVM 设计模式中使用的XZMocoaTableView和XZMocoaCollectionView视图。
UIView 的适配化
在 MVVM 中,UIViewController的角色是View,所以在 Mocoa 中,通过View可以直接获取对应的控制器。
@protocol XZMocoaView <NSObject>
@property (nonatomic, readonly, nullable) __kindof UIViewController *viewController;
@property (nonatomic, readonly, nullable) __kindof UINavigationController *navigationController;
@property (nonatomic, readonly, nullable) __kindof UITabBarController *tabBarController;
@endUITableView/UICollectionView 的适配化
XZMocoaTableView和XZMocoaCollectionView是适配化后的列表视图,仅对UITableView和UICollectionView进行了一次简单的封装。
- 通过
ViewModel管理cell的高度。
@interface XZMocoaTableCellViewModel : XZMocoaListityCellViewModel
@optional
@property (nonatomic) CGFloat height;
@end- 列表事件,重新转发给
cell,并再转发给ViewModel处理。
@interface XZMocoaTableCellViewModel : XZMocoaListityCellViewModel
@optional
- (void)tableView:(XZMocoaTableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(XZMocoaTableView *)tableView willDisplayRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(XZMocoaTableView *)tableView didEndDisplayingRowAtIndexPath:(NSIndexPath*)indexPath;
@endMocoa 目前默认只转发了基本的三个事件,如需要更多事件,需要开发者重写或在Category中自行实现。
- 同步更新视图。
当数据变化后,调用ViewModel相应的方法,即可更新视图。
[_dataArray removeObjectAtIndex:0];
[_tableViewModel deleteSectionAtIndex:0];- 批量更新及自动差异分析。
在传统的列表展示页面中,由于数据是通过服务端请求的,我们很少分析数据进行局部更新,而是在获取到数据后,直接reloadData刷新页面。
现在通过 Mocoa 的自动差异分析功能,直接实现局部更新。
[_tableViewModel performBatchUpdates:^{
[_dataArray removeAllObjects];
[_dataArray addObjectsFromArray:newData];
} completion:^(BOOL finished) {
// do something
}];将更新数据的操作,放在batchUpdates块中,即会自动进行差异分析,并进行局部刷新。
Mocoa 的差异分析功能依赖数据的-isEqual:方法,因此需要在Model中重写此方法。
当然,如果在数据层已经做了数据管理,比如从数据层获取的数据,同一数据始终是同一个对象,或已经做了-isEqual:处理,这一步就可以省略。
- (BOOL)isEqual:(Example20Group102CellModel *)object {
if (object == self) return YES;
if (![object isKindOfClass:[Example20Group102CellModel class]]) return NO;
return [self.nid isEqualToString:object.nid];
}自动差异分析从而,局部刷新的效果,在“示例工程”中有完整的展示以供参考。
Author
Xezun, [email protected]
License
XZMocoa is available under the MIT license. See the LICENSE file for more info.