从简书迁移到掘金...git
本文为回答一位朋友关于MVC/MVP/MVVM架构方面的疑问所写, 旨在介绍iOS下MVC/MVP/MVVM三种架构的设计思路以及各自的优缺点. 全文约五千字, 预计花费阅读时间20 - 30分钟.程序员
概念过完了, 下面来看看, 在具体的业务场景中MVC/MVP/MVVM都是如何表现的.github
//UserVC
- (void)viewDidLoad {
[super viewDidLoad];
[[UserApi new] fetchUserInfoWithUserId:132 completionHandler:^(NSError *error, id result) {
if (error) {
[self showToastWithText:@"获取用户信息失败了~"];
} else {
self.userIconIV.image = ...
self.userSummaryLabel.text = ...
...
}
}];
[[userApi new] fetchUserBlogsWithUserId:132 completionHandler:^(NSError *error, id result) {
if (error) {
[self showErrorInView:self.tableView info:...];
} else {
[self.blogs addObjectsFromArray:result];
[self.tableView reloadData];
}
}];
}
//...略
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BlogCell"];
cell.blog = self.blogs[indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.navigationController pushViewController:[BlogDetailViewController instanceWithBlog:self.blogs[indexPath.row]] animated:YES];
}
//...略
复制代码
//BlogCell
- (void)setBlog:(Blog)blog {
_blog = blog;
self.authorLabel.text = blog.blogAuthor;
self.likeLebel.text = [NSString stringWithFormat:@"赞 %ld", blog.blogLikeCount];
...
}
复制代码
程序员很快写完了代码, Command+R一跑, 没有问题, 心满意足的作其余事情去了. 后来有一天, 产品要求这个业务须要改动, 用户在看他人信息时是上图中的页面, 看本身的信息时, 多一个草稿箱的展现, 像这样:编程
//UserVC
- (void)viewDidLoad {
[super viewDidLoad];
if (self.userId != LoginUserId) {
self.switchButton.hidden = self.draftTableView.hidden = YES;
self.blogTableView.frame = ...
}
[[UserApi new] fetchUserI......略
[[UserApi new] fetchUserBlogsWithUserId:132 completionHandler:^(NSError *error, id result) {
//if Error...略
[self.blogs addObjectsFromArray:result];
[self.blogTableView reloadData];
}];
[[userApi new] fetchUserDraftsWithUserId:132 completionHandler:^(NSError *error, id result) {
//if Error...略
[self.drafts addObjectsFromArray:result];
[self.draftTableView reloadData];
}];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return tableView == self.blogTableView ? self.blogs.count : self.drafts.count;
}
//...略
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.blogTableView) {
BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BlogCell"];
cell.blog = self.blogs[indexPath.row];
return cell;
} else {
DraftCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DraftCell"];
cell.draft = self.drafts[indexPath.row];
return cell;
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.blogTableView) ...
}
//...略
复制代码
//DraftCell
- (void)setDraft:(draft)draft {
_draft = draft;
self.draftEditDate = ...
}
//BlogCell
- (void)setBlog:(Blog)blog {
...同上
}
复制代码
后来啊, 产品以为用户看本身的页面再加个回收站什么的会很好, 因而程序员又加上一段代码逻辑 , 再后来... 随着需求的变动, UserVC变得愈来愈臃肿, 愈来愈难以维护, 拓展性和测试性也极差. 程序员也发现好像代码写得有些问题, 可是问题具体出在哪里? 难道这不是MVC吗? 咱们将上面的过程用一张图来表示:api
另外, 做为V的两个cell直接耦合了M(blog/draft), 这意味着这两个V的输入被绑死到了相应的M上, 复用无从谈起. 最后, 针对这个业务场景的测试异常麻烦, 由于业务初始化和销毁被绑定到了VC的生命周期上, 而相应的逻辑也关联到了和View的点击事件, 测试只能Command+R, 点点点...安全
也许是UIViewController的类名给新人带来了迷惑, 让人误觉得VC就必定是MVC中的C层, 又或许是Button, Label之类的View太过简单彻底不须要一个C层来配合, 总之, 我工做以来经历的项目中见过太多这样的"MVC". 那么, 什么才是正确的MVC使用姿式呢? 仍以上面的业务场景举例, 正确的MVC应该是这个样子的:bash
@interface BlogTableViewHelper : NSObject<UITableViewDelegate, UITableViewDataSource>
+ (instancetype)helperWithTableView:(UITableView *)tableView userId:(NSUInteger)userId;
- (void)fetchDataWithCompletionHandler:(NetworkTaskCompletionHander)completionHander;
- (void)setVCGenerator:(ViewControllerGenerator)VCGenerator;
@end
复制代码
@interface BlogTableViewHelper()
@property (weak, nonatomic) UITableView *tableView;
@property (copy, nonatomic) ViewControllerGenerator VCGenerator;
@property (assign, nonatomic) NSUInteger userId;
@property (strong, nonatomic) NSMutableArray *blogs;
@property (strong, nonatomic) UserAPIManager *apiManager;
@end
#define BlogCellReuseIdentifier @"BlogCell"
@implementation BlogTableViewHelper
+ (instancetype)helperWithTableView:(UITableView *)tableView userId:(NSUInteger)userId {
return [[BlogTableViewHelper alloc] initWithTableView:tableView userId:userId];
}
- (instancetype)initWithTableView:(UITableView *)tableView userId:(NSUInteger)userId {
if (self = [super init]) {
self.userId = userId;
tableView.delegate = self;
tableView.dataSource = self;
self.apiManager = [UserAPIManager new];
self.tableView = tableView;
__weak typeof(self) weakSelf = self;
[tableView registerClass:[BlogCell class] forCellReuseIdentifier:BlogCellReuseIdentifier];
tableView.header = [MJRefreshAnimationHeader headerWithRefreshingBlock:^{//下拉刷新
[weakSelf.apiManage refreshUserBlogsWithUserId:userId completionHandler:^(NSError *error, id result) {
//...略
}];
}];
tableView.footer = [MJRefreshAnimationFooter headerWithRefreshingBlock:^{//上拉加载
[weakSelf.apiManage loadMoreUserBlogsWithUserId:userId completionHandler:^(NSError *error, id result) {
//...略
}];
}];
}
return self;
}
#pragma mark - UITableViewDataSource && Delegate
//...略
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.blogs.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogCellReuseIdentifier];
BlogCellHelper *cellHelper = self.blogs[indexPath.row];
if (!cell.didLikeHandler) {
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
cellHelper.likeCount += 1;
weakCell.likeCountText = cellHelper.likeCountText;
}];
}
cell.authorText = cellHelper.authorText;
//...各类设置
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.navigationController pushViewController:self.VCGenerator(self.blogs[indexPath.row]) animated:YES];
}
#pragma mark - Utils
- (void)fetchDataWithCompletionHandler:(NetworkTaskCompletionHander)completionHander {
[[UserAPIManager new] refreshUserBlogsWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
[self showErrorInView:self.tableView info:error.domain];
} else {
for (Blog *blog in result) {
[self.blogs addObject:[BlogCellHelper helperWithBlog:blog]];
}
[self.tableView reloadData];
}
completionHandler ? completionHandler(error, result) : nil;
}];
}
//...略
@end
复制代码
@implementation BlogCell
//...略
- (void)onClickLikeButton:(UIButton *)sender {
[[UserAPIManager new] likeBlogWithBlogId:self.blogId userId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
//do error
} else {
//do success
self.didLikeHandler ? self.didLikeHandler() : nil;
}
}];
}
@end
复制代码
@implementation BlogCellHelper
- (NSString *)likeCountText {
return [NSString stringWithFormat:@"赞 %ld", self.blog.likeCount];
}
//...略
- (NSString *)authorText {
return [NSString stringWithFormat:@"做者姓名: %@", self.blog.authorName];
}
@end
复制代码
Blog模块由BlogTableViewHelper(C), BlogTableView(V), Blogs(C)构成, 这里有点特殊, blogs里面装的不是M, 而是Cell的C层CellHelper, 这是由于Blog的MVC其实又是由多个更小的MVC组成的. M和V没什么好说的, 主要说一下做为C的TableVIewHelper作了什么.服务器
实际开发中, 各个模块的View多是在Scene对应的Storyboard中新建并布局的, 此时就不用各个模块本身创建View了(好比这里的BlogTableViewHelper), 让Scene传到C层进行管理就好了, 固然, 若是你是纯代码的方式, 那View就须要相应模块自行创建了(好比下文的UserInfoViewController), 这个看本身的意愿, 无伤大雅.微信
BlogTableViewHelper对外提供获取数据和必要的构造方法接口, 内部根据自身状况进行相应的初始化.架构
当外部调用fetchData的接口后, Helper就会启动获取数据逻辑, 由于数据获取先后可能会涉及到一些页面展现(HUD之类的), 而具体的展现又是和Scene直接相关的(有的Scene展现的是HUD有的可能展现的又是一种样式或者根本不展现), 因此这部分会以CompletionHandler的形式交由Scene本身处理.
在Helper内部, 数据获取失败会展现相应的错误页面, 成功则创建更小的MVC部分并通知其展现数据(也就是通知CellHelper驱动Cell), 另外, TableView的上拉刷新和下拉加载逻辑也是隶属于Blog模块的, 因此也在Helper中处理. 在页面跳转的逻辑中, 点击跳转的页面是由Scene经过VCGeneratorBlock直接配置的, 因此也是解耦的(你也能够经过didSelectRowHandler之类的方式传递数据到Scene层, 由Scene作跳转, 是同样的).
最后, V(Cell)如今只暴露了Set方法供外部进行设置, 因此和M(Blog)之间也是隔离的, 复用没有问题.
这一系列过程都是自管理的, 未来若是Blog模块会在另外一个SceneX展现, 那么SceneX只须要新建一个BlogTableViewHelper, 而后调用一下helper.fetchData便可.
DraftTableViewHelper和BlogTableViewHelper逻辑相似, 就不贴了, 简单贴一下UserInfo模块的逻辑:
@implementation UserInfoViewController
+ (instancetype)instanceUserId:(NSUInteger)userId {
return [[UserInfoViewController alloc] initWithUserId:userId];
}
- (instancetype)initWithUserId:(NSUInteger)userId {
// ...略
[self addUI];
// ...略
}
#pragma mark - Action
- (void)onClickIconButton:(UIButton *)sender {
[self.navigationController pushViewController:self.VCGenerator(self.user) animated:YES];
}
#pragma mark - Utils
- (void)addUI {
//各类UI初始化 各类布局
self.userIconIV = [[UIImageView alloc] initWithFrame:CGRectZero];
self.friendCountLabel = ...
...
}
- (void)fetchData {
[[UserAPIManager new] fetchUserInfoWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
[self showErrorInView:self.view info:error.domain];
} else {
self.user = [User objectWithKeyValues:result];
self.userIconIV.image = [UIImage imageWithURL:[NSURL URLWithString:self.user.url]];//数据格式化
self.friendCountLabel.text = [NSString stringWithFormat:@"赞 %ld", self.user.friendCount];//数据格式化
...
}
}];
}
@end
复制代码
UserInfoViewController除了比两个TableViewHelper多个addUI的子控件布局方法, 其余逻辑大同小异, 也是本身管理的MVC, 也是只须要初始化便可在任何一个Scene中使用.
如今三个自管理模块已经创建完成, UserVC须要的只是根据本身的状况作相应的拼装布局便可, 就和搭积木同样:
@interface UserViewController ()
@property (assign, nonatomic) NSUInteger userId;
@property (strong, nonatomic) UserInfoViewController *userInfoVC;
@property (strong, nonatomic) UITableView *blogTableView;
@property (strong, nonatomic) BlogTableViewHelper *blogTableViewHelper;
@end
@interface SelfViewController : UserViewController
@property (strong, nonatomic) UITableView *draftTableView;
@property (strong, nonatomic) DraftTableViewHelper *draftTableViewHelper;
@end
#pragma mark - UserViewController
@implementation UserViewController
+ (instancetype)instanceWithUserId:(NSUInteger)userId {
if (userId == LoginUserId) {
return [[SelfViewController alloc] initWithUserId:userId];
} else {
return [[UserViewController alloc] initWithUserId:userId];
}
}
- (void)viewDidLoad {
[super viewDidLoad];
[self addUI];
[self configuration];
[self fetchData];
}
#pragma mark - Utils(UserViewController)
- (void)addUI {
//这里只是表达一下意思 具体的layout逻辑确定不是这么简单的
self.userInfoVC = [UserInfoViewController instanceWithUserId:self.userId];
self.userInfoVC.view.frame = CGRectZero;
[self.view addSubview:self.userInfoVC.view];
[self.view addSubview:self.blogTableView = [[UITableView alloc] initWithFrame:CGRectZero style:0]];
}
- (void)configuration {
self.title = @"用户详情";
// ...其余设置
[self.userInfoVC setVCGenerator:^UIViewController *(id params) {
return [UserDetailViewController instanceWithUser:params];
}];
self.blogTableViewHelper = [BlogTableViewHelper helperWithTableView:self.blogTableView userId:self.userId];
[self.blogTableViewHelper setVCGenerator:^UIViewController *(id params) {
return [BlogDetailViewController instanceWithBlog:params];
}];
}
- (void)fetchData {
[self.userInfoVC fetchData];//userInfo模块不须要任何页面加载提示
[HUD show];//blog模块可能就须要HUD
[self.blogTableViewHelper fetchDataWithcompletionHandler:^(NSError *error, id result) {
[HUD hide];
}];
}
@end
#pragma mark - SelfViewController
@implementation SelfViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self addUI];
[self configuration];
[self fetchData];
}
#pragma mark - Utils(SelfViewController)
- (void)addUI {
[super addUI];
[self.view addSubview:switchButton];//特有部分...
//...各类设置
[self.view addSubview:self.draftTableView = [[UITableView alloc] initWithFrame:CGRectZero style:0]];
}
- (void)configuration {
[super configuration];
self.draftTableViewHelper = [DraftTableViewHelper helperWithTableView:self.draftTableView userId:self.userId];
[self.draftTableViewHelper setVCGenerator:^UIViewController *(id params) {
return [DraftDetailViewController instanceWithDraft:params];
}];
}
- (void)fetchData {
[super fetchData];
[self.draftTableViewHelper fetchData];
}
@end
复制代码
做为业务场景的的Scene(UserVC)作的事情很简单, 根据自身状况对三个模块进行配置(configuration), 布局(addUI), 而后通知各个模块启动(fetchData)就能够了, 由于每一个模块的展现和交互是自管理的, 因此Scene只须要负责和自身业务强相关的部分便可. 另外, 针对自身访问的状况咱们创建一个UserVC子类SelfVC, SelfVC作的也是相似的事情.
MVC到这就说的差很少了, 对比上面错误的MVC方式, 咱们看看解决了哪些问题:
1.代码复用: 三个小模块的V(cell/userInfoView)对外只暴露Set方法, 对M甚至C都是隔离状态, 复用彻底没有问题.三个大模块的MVC也能够用于快速构建类似的业务场景(大模块的复用比小模块会差一些, 下文我会说明).
2.代码臃肿: 由于Scene大部分的逻辑和布局都转移到了相应的MVC中, 咱们仅仅是拼装MVC的便构建了两个不一样的业务场景, 每一个业务场景都能正常的进行相应的数据展现, 也有相应的逻辑交互, 而完成这些东西, 加空格也就100行代码左右(固然, 这里我忽略了一下Scene的布局代码).
3.易拓展性: 不管产品将来想加回收站仍是防护塔, 我须要的只是新建相应的MVC模块, 加到对应的Scene便可.
4.可维护性: 各个模块间职责分离, 哪里出错改哪里, 彻底不影响其余模块. 另外, 各个模块的代码其实并不算多, 哪一天即便写代码的人离职了, 接手的人根据错误提示也能快速定位出错模块.
5.易测试性: 很遗憾, 业务的初始化依然绑定在Scene的生命周期中, 而有些逻辑也仍然须要UI的点击事件触发, 咱们依然只能Command+R, 点点点...
能够看到, 即便是标准的MVC架构也并不是完美, 仍然有部分问题难以解决, 那么MVC的缺点何在? 总结以下: 1.过分的注重隔离: 这个其实MV(x)系列都有这缺点, 为了实现V层的彻底隔离, V对外只暴露Set方法, 通常状况下没什么问题, 可是当须要设置的属性不少时, 大量重复的Set方法写起来仍是很累人的.
2.业务逻辑和业务展现强耦合: 能够看到, 有些业务逻辑(页面跳转/点赞/分享...)是直接散落在V层的, 这意味着咱们在测试这些逻辑时, 必须首先生成对应的V, 而后才能进行测试. 显然, 这是不合理的. 由于业务逻辑最终改变的是数据M, 咱们的关注点应该在M上, 而不是展现M的V.
MVC的缺点在于并无区分业务逻辑和业务展现, 这对单元测试很不友好. MVP针对以上缺点作了优化, 它将业务逻辑和业务展现也作了一层隔离, 对应的就变成了MVCP. M和V功能不变, 原来的C如今只负责布局, 而全部的逻辑全都转移到了P层.
对应关系如图所示:
业务场景没有变化, 依然是展现三种数据, 只是三个MVC替换成了三个MVP(图中我只画了Blog模块), UserVC负责配置三个MVP(新建各自的VP, 经过VP创建C, C会负责创建VP之间的绑定关系), 并在合适的时机通知各自的P层(以前是通知C层)进行数据获取, 各个P层在获取到数据后进行相应处理, 处理完成后会通知绑定的View数据有所更新, V收到更新通知后从P获取格式化好的数据进行页面渲染, UserVC最后将已经渲染好的各个View进行布局便可.
另外, V层C层再也不处理任何业务逻辑, 全部事件触发所有调用P层的相应命令, 具体到代码中以下:
@interface BlogPresenter : NSObject
+ (instancetype)instanceWithUserId:(NSUInteger)userId;
- (NSArray *)allDatas;//业务逻辑移到了P层 和业务相关的M也跟着到了P层
- (void)refreshUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
- (void)loadMoreUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
@end
复制代码
@interface BlogPresenter()
@property (assign, nonatomic) NSUInteger userId;
@property (strong, nonatomic) NSMutableArray *blogs;
@property (strong, nonatomic) UserAPIManager *apiManager;
@end
@implementation BlogPresenter
+ (instancetype)instanceWithUserId:(NSUInteger)userId {
return [[BlogPresenter alloc] initWithUserId:userId];
}
- (instancetype)initWithUserId:(NSUInteger)userId {
if (self = [super init]) {
self.userId = userId;
self.apiManager = [UserAPIManager new];
//...略
}
}
#pragma mark - Interface
- (NSArray *)allDatas {
return self.blogs;
}
//提供给外层调用的命令
- (void)refreshUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler {
[self.apiManager refreshUserBlogsWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (!error) {
[self.blogs removeAllObjects];//清空以前的数据
for (Blog *blog in result) {
[self.blogs addObject:[BlogCellPresenter presenterWithBlog:blog]];
}
}
completionHandler ? completionHandler(error, result) : nil;
}];
}
//提供给外层调用的命令
- (void)loadMoreUserBlogsWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler {
[self.apiManager loadMoreUserBlogsWithUserId:self.userId completionHandler...]
}
@end
复制代码
@interface BlogCellPresenter : NSObject
+ (instancetype)presenterWithBlog:(Blog *)blog;
- (NSString *)authorText;
- (NSString *)likeCountText;
- (void)likeBlogWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
- (void)shareBlogWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler;
@end
复制代码
@implementation BlogCellPresenter
- (NSString *)likeCountText {
return [NSString stringWithFormat:@"赞 %ld", self.blog.likeCount];
}
- (NSString *)authorText {
return [NSString stringWithFormat:@"做者姓名: %@", self.blog.authorName];
}
// ...略
- (void)likeBlogWithCompletionHandler:(NetworkTaskCompletionHander)completionHandler {
[[UserAPIManager new] likeBlogWithBlogId:self.blogId userId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
//do fail
} else {
//do success
self.blog.likeCount += 1;
}
completionHandler ? completionHandler(error, result) : nil;
}];
}
// ...略
@end
复制代码
BlogPresenter和BlogCellPresenter分别做为BlogViewController和BlogCell的P层, 其实就是一系列业务逻辑的集合.
BlogPresenter负责获取Blogs原始数据并经过这些原始数据构造BlogCellPresenter, 而BlogCellPresenter提供格式化好的各类数据以供Cell渲染, 另外, 点赞和分享的业务如今也转移到了这里.
业务逻辑被转移到了P层, 此时的V层只须要作两件事:
1.监听P层的数据更新通知, 刷新页面展现.
2.在点击事件触发时, 调用P层的对应方法, 并对方法执行结果进行展现.
@interface BlogCell : UITableViewCell
@property (strong, nonatomic) BlogCellPresenter *presenter;
@end
复制代码
@implementation BlogCell
- (void)setPresenter:(BlogCellPresenter *)presenter {
_presenter = presenter;
//从Presenter获取格式化好的数据进行展现
self.authorLabel.text = presenter.authorText;
self.likeCountLebel.text = presenter.likeCountText;
// ...略
}
#pragma mark - Action
- (void)onClickLikeButton:(UIButton *)sender {
[self.presenter likeBlogWithCompletionHandler:^(NSError *error, id result) {
if (!error) {//页面刷新
self.likeCountLebel.text = self.presenter.likeCountText;
}
// ...略
}];
}
@end
复制代码
而C层作的事情就是布局和PV之间的绑定(这里可能不太明显, 由于BlogVC里面的布局代码是TableViewDataSource, PV绑定的话, 由于我偷懒用了Block作通知回调, 因此也不太明显, 若是是Protocol回调就很明显了), 代码以下:
@interface BlogViewController : NSObject
+ (instancetype)instanceWithTableView:(UITableView *)tableView presenter:(BlogPresenter)presenter;
- (void)setDidSelectRowHandler:(void (^)(Blog *))didSelectRowHandler;
- (void)fetchDataWithCompletionHandler:(NetworkCompletionHandler)completionHandler;
@end
复制代码
@interface BlogViewController ()<UITableViewDataSource, UITabBarDelegate, BlogView>
@property (weak, nonatomic) UITableView *tableView;
@property (strong, nonatomic) BlogPresenter presenter;
@property (copy, nonatomic) void(^didSelectRowHandler)(Blog *);
@end
@implementation BlogViewController
+ (instancetype)instanceWithTableView:(UITableView *)tableView presenter:(BlogPresenter)presenter {
return [[BlogViewController alloc] initWithTableView:tableView presenter:presenter];
}
- (instancetype)initWithTableView:(UITableView *)tableView presenter:(BlogPresenter)presenter {
if (self = [super init]) {
self.presenter = presenter;
self.tableView = tableView;
tableView.delegate = self;
tableView.dataSource = self;
__weak typeof(self) weakSelf = self;
[tableView registerClass:[BlogCell class] forCellReuseIdentifier:BlogCellReuseIdentifier];
tableView.header = [MJRefreshAnimationHeader headerWithRefreshingBlock:^{//下拉刷新
[weakSelf.presenter refreshUserBlogsWithCompletionHandler:^(NSError *error, id result) {
[weakSelf.tableView.header endRefresh];
if (!error) {
[weakSelf.tableView reloadData];
}
//...略
}];
}];
tableView.footer = [MJRefreshAnimationFooter headerWithRefreshingBlock:^{//上拉加载
[weakSelf.presenter loadMoreUserBlogsWithCompletionHandler:^(NSError *error, id result) {
[weakSelf.tableView.footer endRefresh];
if (!error) {
[weakSelf.tableView reloadData];
}
//...略
}];
}];
}
return self;
}
#pragma mark - Interface
- (void)fetchDataWithCompletionHandler:(NetworkCompletionHandler)completionHandler {
[self.presenter refreshUserBlogsWithCompletionHandler:^(NSError *error, id result) {
if (error) {
//show error info
} else {
[self.tableView reloadData];
}
completionHandler ? completionHandler(error, result) : nil;
}];
}
#pragma mark - UITableViewDataSource && Delegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.presenter.allDatas.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCell *cell = [tableView dequeueReusableCellWithIdentifier:BlogCellReuseIdentifier];
BlogCellPresenter *cellPresenter = self.presenter.allDatas[indexPath.row];
cell.present = cellPresenter;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.didSelectRowHandler ? self.didSelectRowHandler(self.presenter.allDatas[indexPath.row]) : nil;
}
@end
复制代码
BlogViewController如今再也不负责实际的数据获取逻辑, 数据获取直接调用Presenter的相应接口, 另外, 由于业务逻辑也转移到了Presenter, 因此TableView的布局用的也是Presenter.allDatas. 至于Cell的展现, 咱们替换了原来大量的Set方法, 让Cell本身根据绑定的CellPresenter作展现. 毕竟如今逻辑都移到了P层, V层要作相应的交互也必须依赖对应的P层命令, 好在V和M仍然是隔离的, 只是和P耦合了, P层是能够随意替换的, M显然不行, 这是一种折中.
最后是Scene, 它的变更不大, 只是替换配置MVC为配置MVP, 另外数据获取也是走P层, 不走C层了(然而代码里面并非这样的):
- (void)configuration {
// ...其余设置
BlogPresenter *blogPresenter = [BlogPresenter instanceWithUserId:self.userId];
self.blogViewController = [BlogViewController instanceWithTableView:self.blogTableView presenter:blogPresenter];
[self.blogViewController setDidSelectRowHandler:^(Blog *blog) {
[self.navigationController pushViewController:[BlogDetailViewController instanceWithBlog:blog] animated:YES];
}];
// ...略
}
- (void)fetchData {
// ...略
[self.userInfoVC fetchData];
[HUD show];
[self.blogViewController fetchDataWithCompletionHandler:^(NSError *error, id result) {
[HUD hide];
}];
//仍是由于懒, 用了Block走C层转发会少写一些代码, 若是是Protocol或者KVO方式就会用self.blogViewController.presenter了
//不过没有关系, 由于咱们替换MVC为MVP是为了解决单元测试的问题, 如今的用法彻底不影响单元测试, 只是和概念不符罢了.
// ...略
}
复制代码
上面的例子中其实有一个问题, 即咱们假定: 全部的事件都是由V层主动发起且一次性的. 这实际上是不成立的, 举个简单的例子: 相似微信语音聊天之类的页面, 点击语音Cell开始播放, Cell展现播放动画, 播放完成动画中止, 而后播放下一条语音.
在这个播放场景中, 若是CellPresenter仍是像上面同样仅仅提供一个playWithCompletionHandler的接口是行不通的. 由于播放完成后回调确定是在C层, C层在播放完成后会发现此时执行播放命令的CellPresenter没法通知Cell中止动画, 即事件的触发不是一次性的. 另外, 在播放完成后, C层遍历到下一个待播放CellPresenterX调用播放接口时, CellPresenterX由于并不知道它对应的Cell是谁, 固然也就没法通知Cell开始动画, 即事件的发起者并不必定是V层.
针对这些非一次性或者其余层发起事件, 处理方法其实很简单, 在CellPresenter加个Block属性就好了, 由于是属性, Block能够屡次回调, 另外Block还能够捕获Cell, 因此也不担忧找不到对应的Cell. 大概这样:
@interface VoiceCellPresenter : NSObject
@property (copy, nonatomic) void(^didUpdatePlayStateHandler)(NSUInteger);
- (NSURL *)playURL;
@end
复制代码
@implementation VoiceCell
- (void)setPresenter:(VoiceCellPresenter *)presenter {
_presenter = presenter;
if (!presenter.didUpdatePlayStateHandler) {
__weak typeof(self) weakSelf = self;
[presenter setDidUpdatePlayStateHandler:^(NSUInteger playState) {
switch (playState) {
case Buffering: weakSelf.playButton... break;
case Playing: weakSelf.playButton... break;
case Paused: weakSelf.playButton... break;
}
}];
}
}
复制代码
播放的时候, VC只须要保持一下CellPresenter, 而后传入相应的playState调用didUpdatePlayStateHandler就能够更新Cell的状态了. 固然, 若是是Protocol的方式进行的VP绑定, 那么作这些事情就很日常了, 就不写了.
MVP大概就是这个样子了, 相对于MVC, 它其实只作了一件事情, 即分割业务展现和业务逻辑. 展现和逻辑分开后, 只要咱们能保证V在收到P的数据更新通知后能正常刷新页面, 那么整个业务就没有问题. 由于V收到的通知其实都是来自于P层的数据获取/更新操做, 因此咱们只要保证P层的这些操做都是正常的就能够了. 即咱们只用测试P层的逻辑, 没必要关心V层的状况.
MVP其实已是一个很好的架构, 几乎解决了全部已知的问题, 那么为何还会有MVVM呢? 仍然是举例说明, 假设如今有一个Cell, 点击Cell上面的关注按钮能够是加关注, 也能够是取消关注, 在取消关注时, SceneA要求先弹窗询问, 而SceneB则不作弹窗, 那么此时的取消关注操做就和业务场景强关联, 因此这个接口不多是V层直接调用, 会上升到Scene层.具体到代码中, 大概这个样子:
@interface UserCellPresenter : NSObject
@property (copy, nonatomic) void(^followStateHander)(BOOL isFollowing);
@property (assign, nonatomic) BOOL isFollowing;
- (void)follow;
@end
复制代码
@implementation UserCellPresenter
- (void)follow {
if (!self.isFollowing) {//未关注 去关注
// follow user
} else {//已关注 则取消关注
self.followStateHander ? self.followStateHander(YES) : nil;//先通知Cell显示follow状态
[[FollowAPIManager new] unfollowWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
self.followStateHander ? self.followStateHander(NO) : nil;//follow失败 状态回退
} eles {
self.isFollowing = YES;
}
//...略
}];
}
}
@end
复制代码
@implementation UserCell
- (void)setPresenter:(UserCellPresenter *)presenter {
_presenter = presenter;
if (!_presenter.followStateHander) {
__weak typeof(self) weakSelf = self;
[_presenter setFollowStateHander:^(BOOL isFollowing) {
[weakSelf.followStateButton setImage:isFollowing ? : ...];
}];
}
}
- (void)onClickFollowButton:(UIButton *)button {//将关注按钮点击事件上传
[self routeEvent:@"followEvent" userInfo:@{@"presenter" : self.presenter}];
}
@end
复制代码
@implementation FollowListViewController
//拦截点击事件 判断后确认是否执行事件
- (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
if ([eventName isEqualToString:@"followEvent"]) {
UserCellPresenter *presenter = userInfo[@"presenter"];
[self showAlertWithTitle:@"提示" message:@"确认取消对他的关注吗?" cancelHandler:nil confirmHandler: ^{
[presenter follow];
}];
}
}
@end
复制代码
@implementation UIResponder (Router)
//沿着响应者链将事件上传 事件最终被拦截处理 或者 无人处理直接丢弃
- (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
[self.nextResponder routeEvent:eventName userInfo:userInfo];
}
@end
复制代码
Block方式看起来略显繁琐, 咱们换到Protocol看看:
@protocol UserCellPresenterCallBack <NSObject>
- (void)userCellPresenterDidUpdateFollowState:(BOOL)isFollowing;
@end
@interface UserCellPresenter : NSObject
@property (weak, nonatomic) id<UserCellPresenterCallBack> view;
@property (assign, nonatomic) BOOL isFollowing;
- (void)follow;
@end
复制代码
@implementation UserCellPresenter
- (void)follow {
if (!self.isFollowing) {//未关注 去关注
// follow user
} else {//已关注 则取消关注
BOOL isResponse = [self.view respondsToSelector:@selector(userCellPresenterDidUpdateFollowState)];
isResponse ? [self.view userCellPresenterDidUpdateFollowState:YES] : nil;
[[FollowAPIManager new] unfollowWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) {
isResponse ? [self.view userCellPresenterDidUpdateFollowState:NO] : nil;
} eles {
self.isFollowing = YES;
}
//...略
}];
}
}
@end
复制代码
@implementation UserCell
- (void)setPresenter:(UserCellPresenter *)presenter {
_presenter = presenter;
_presenter.view = self;
}
#pragma mark - UserCellPresenterCallBack
- (void)userCellPresenterDidUpdateFollowState:(BOOL)isFollowing {
[self.followStateButton setImage:isFollowing ? : ...];
}
复制代码
除去Route和VC中Alert之类的代码, 能够发现不管是Block方式仍是Protocol方式由于须要对页面展现和业务逻辑进行隔离, 代码上饶了一小圈, 无形中增添了很多的代码量, 这里仅仅只是一个事件就这样, 若是是多个呢? 那写起来真是蛮伤的...
仔细看一下上面的代码就会发现, 若是咱们继续添加事件, 那么大部分的代码都是在作一件事情: P层将数据更新通知到V层.
Block方式会在P层添加不少属性, 在V层添加不少设置Block逻辑. 而Protocol方式虽然P层只添加了一个属性, 可是Protocol里面的方法却会一直增长, 对应的V层也就须要增长的方法实现.
问题既然找到了, 那就试着去解决一下吧, OC中可以实现两个对象间的低耦合通讯, 除了Block和Protocol, 通常都会想到KVO. 咱们看看KVO在上面的例子有何表现:
@interface UserCellViewModel : NSObject
@property (assign, nonatomic) BOOL isFollowing;
- (void)follow;
@end
复制代码
@implementation UserCellViewModel
- (void)follow {
if (!self.isFollowing) {//未关注 去关注
// follow user
} else {//已关注 则取消关注
self.isFollowing = YES;//先通知Cell显示follow状态
[[FollowAPIManager new] unfollowWithUserId:self.userId completionHandler:^(NSError *error, id result) {
if (error) { self.isFollowing = NO; }//follow失败 状态回退
//...略
}];
}
}
@end
复制代码
@implementation UserCell
- (void)awakeFromNib {
@weakify(self);
[RACObserve(self, viewModel.isFollowing) subscribeNext:^(NSNumber *isFollowing) {
@strongify(self);
[self.followStateButton setImage:[isFollowing boolValue] ? : ...];
};
}
复制代码
代码大概少了一半左右, 另外, 逻辑读起来也清晰多了, Cell观察绑定的ViewModel的isFollowing状态, 并在状态改变时, 更新本身的展现. 三种数据通知方式简单一比对, 相信哪一种方式对程序员更加友好, 你们都内心有数, 就不作赘述了.
如今大概一提到MVVM就会想到RAC, 但这二者其实并无什么联系, 对于MVVM而言RAC只是提供了优雅安全的数据绑定方式, 若是不想学RAC, 本身搞个KVOHelper之类的东西也是能够的. 另外 ,RAC的魅力其实在于函数式响应式编程, 咱们不该该仅仅将它局限于MVVM的应用, 平常的开发中也应该多使用使用的.
关于MVVM, 我想说的就是这么多了, 由于MVVM其实只是MVP的绑定进化体, 除去数据绑定方式, 其余的和MVP一模一样, 只是可能呈现方式是Command/Signal而不是CompletionHandler之类的, 故不作赘述.
最后作个简单的总结吧:
1.MVC做为老牌架构, 优势在于将业务场景按展现数据类型划分出多个模块, 每一个模块中的C层负责业务逻辑和业务展现, 而M和V应该是互相隔离的以作重用, 另外每一个模块处理得当也能够做为重用单元. 拆分在于解耦, 顺便作了减负, 隔离在于重用, 提高开发效率. 缺点是没有区分业务逻辑和业务展现, 对单元测试不友好.
2.MVP做为MVC的进阶版, 提出区分业务逻辑和业务展现, 将全部的业务逻辑转移到P层, V层接受P层的数据更新通知进行页面展现. 优势在于良好的分层带来了友好的单元测试, 缺点在于分层会让代码逻辑优势绕, 同时也带来了大量的代码工做, 对程序员不够友好.
3.MVVM做为集大成者, 经过数据绑定作数据更新, 减小了大量的代码工做, 同时优化了代码逻辑, 只是学习成本有点高, 对新手不够友好.
4.MVP和MVVM由于分层因此会创建MVC两倍以上的文件类, 须要良好的代码管理方式.
5.在MVP和MVVM中, V和P或者VM之间理论上是多对多的关系, 不一样的布局在相同的逻辑下只须要替换V层, 而相同的布局不一样的逻辑只须要替换P或者VM层. 但实际开发中P或者VM每每由于耦合了V层的展现逻辑退化成了一对一关系(好比SceneA中须要显示"xxx+Name", VM就将Name格式化为"xxx + Name". 某一天SceneB也用到这个模块, 全部的点击事件和页面展现都同样, 只是Name展现为"yyy + Name", 此时的VM由于耦合SceneA的展现逻辑, 就显得比较尴尬), 针对此类状况, 一般有两种办法, 一种是在VM层加状态进而判断输出状态, 一种是在VM层外再加一层FormatHelper. 前者可能由于状态过多显得代码难看, 后者虽然比较优雅且拓展性高, 可是过多的分层在数据还原时就略显笨拙, 你们应该按需选择.
这里随便瞎扯一句, 有些文章上来就说MVVM是为了解决C层臃肿, MVC难以测试的问题, 其实并非这样的. 按照架构演进顺序来看, C层臃肿大部分是没有拆分好MVC模块, 好好拆分就好了, 用不着MVVM. 而MVC难以测试也能够用MVP来解决, 只是MVP也并不是完美, 在VP之间的数据交互太繁琐, 因此才引出了MVVM. 当MVVM这个彻底体出现之后, 咱们从结果看起源, 发现它作了好多事情, 其实并非, 它的前辈们付出的努力也并很多!
无论是MVC, MVP, MVVM仍是MVXXX, 最终的目的在于服务于人, 咱们注重架构, 注重分层都是为了开发效率, 说到底仍是为了开心. 因此, 在实际开发中不该该拘泥于某一种架构, 根据实际项目出发, 通常普通的MVC就能应对大部分的开发需求, 至于MVP和MVVM, 能够尝试, 但不要强制. 总之, 但愿你们能作到: 设计时, 心中有数. 撸码时, 开心就好.
=======================分割线=======================
这篇博客放出来之后, 陆陆续续收到一些小伙伴的简信, 大部分都是一些提问, 可是每一个人的问题都比较雷同, 趁着清明放假有时间, 这里将问得比较频繁的问题择出来, 这样之后相似的问题就不用一一回复了, 算是解个懒.
Q: 为何有的时候是MVP/MVVM有的时候是MVCP/MVCVM.
A: 其实这个问题在demo的MVP部分有作解释, 估计有些朋友没有看demo, 或者是我描述得太含糊了没能让人明白. 那么这里我分别给出MVVM和MVCVM的例子, 结合代码解释会方便一些, 顺便也回答一下UserInfo模块用MVVM怎么写.
@interface UserInfoViewModel : NSObject
+ (instancetype)viewModelWithUserId:(NSUInteger)userId;
- (User *)user;
- (RACCommand *)fetchUserInfoCommand;
- (UIImage *)icon;
- (NSString *)name;
- (NSString *)summary;
- (NSString *)blogCount;
- (NSString *)friendCount;
@end
复制代码
@interface UserInfoViewModel ()
@property (strong, nonatomic) UIImage *icon;
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *summary;
@property (copy, nonatomic) NSString *blogCount;
@property (copy, nonatomic) NSString *friendCount;
@property (strong, nonatomic) User *user;
@property (assign, nonatomic) NSUInteger userId;
@end
@implementation UserInfoViewModel
+ (instancetype)viewModelWithUserId:(NSUInteger)userId {
UserInfoViewModel *viewModel = [UserInfoViewModel new];
viewModel.userId = userId;
return viewModel;
}
- (RACCommand *)fetchUserInfoCommand {
return [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[self fetchUserInfoSignal] doNext:^(User *user) {
self.user = user;
self.icon = [UIImage imageNamed:user.icon ?: @"icon0"];
self.name = user.name.length > 0 ? user.name : @"匿名";
self.summary = [NSString stringWithFormat:@"我的简介: %@", user.summary.length > 0 ? user.summary : @"这我的很懒, 什么也没有写~"];
self.blogCount = [NSString stringWithFormat:@"做品: %ld", user.blogCount];
self.friendCount = [NSString stringWithFormat:@"好友: %ld", user.friendCount];
}];
}];
}
- (RACSignal *)fetchUserInfoSignal {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[UserAPIManager new] fetchUserInfoWithUserId:self.user.userId completionHandler:^(NSError *error, id result) {
if (!error) {
[subscriber sendNext:result];
[subscriber sendCompleted];
} else {
[subscriber sendError:error];
}
}];
return nil;
}];
}
复制代码
UserInfoViewModel作的事情很简单, 从服务器拉取数据, 而后将数据格式化为V层须要展现的样子, 这部分MVVM和MVCVM都是同样的, 接下来咱们看看不同的部分, 先看看MVVM中的V层代码:
#import "UserInfoViewModel.h"
@interface UserInfoView : UIView
+ (instancetype)instanceWithViewModel:(UserInfoViewModel *)viewModel;
- (void)fetchData;
- (void)setOnClickIconCommand:(RACCommand *)onClickIconCommand;
@end
复制代码
@interface UserInfoView ()
@property (weak, nonatomic) UIButton *iconButton;
@property (weak, nonatomic) UILabel *nameLabel;
@property (weak, nonatomic) UILabel *summaryLabel;
@property (weak, nonatomic) UILabel *blogCountLabel;
@property (weak, nonatomic) UILabel *friendCountLabel;
@property (strong, nonatomic) RACCommand *onClickIconCommand;
@property (strong, nonatomic) UserInfoViewModel *viewModel;
@end
@implementation UserInfoView
+ (instancetype)instanceWithViewModel:(UserInfoViewModel *)viewModel {
UserInfoView *view = [UserInfoView new];
view.viewModel = viewModel;
return view;
}
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self addUI];
[self bind];
}
return self;
}
- (void)bind {
RAC(self.nameLabel, text) = RACObserve(self, viewModel.name);
RAC(self.summaryLabel, text) = RACObserve(self, viewModel.summary);
RAC(self.blogCountLabel, text) = RACObserve(self, viewModel.blogCount);
RAC(self.friendCountLabel, text) = RACObserve(self, viewModel.friendCount);
@weakify(self);
[RACObserve(self, viewModel.icon) subscribeNext:^(UIImage *icon) {
@strongify(self);
[self.iconButton setImage:icon forState:UIControlStateNormal];
}];
[[self.iconButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
@strongify(self);
[self.onClickIconCommand execute:self.viewModel.user];
}];
}
- (void)fetchData {
[[[self.viewModel fetchUserInfoCommand] execute:nil] subscribeError:^(NSError *error) {
//show error view
} completed:^{
//do completed
}];
}
- (void)addUI {
//... 各类新建 各类布局
}
@end
复制代码
而后再看看MVCVM中V层代码:
@interface UserInfoView : UIView
- (UIButton *)iconButton;
- (UILabel *)nameLabel;
- (UILabel *)summaryLabel;
- (UILabel *)blogCountLabel;
- (UILabel *)friendCountLabel;
@end
复制代码
@interface UserInfoView ()
@property (weak, nonatomic) UIButton *iconButton;
@property (weak, nonatomic) UILabel *nameLabel;
@property (weak, nonatomic) UILabel *summaryLabel;
@property (weak, nonatomic) UILabel *blogCountLabel;
@property (weak, nonatomic) UILabel *friendCountLabel;
@end
@implementation UserInfoView
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self addUI];
}
return self;
}
- (void)addUI {
//... 各类新建 各类布局
}
@end
复制代码
在MVVM中的UserInfoView一共作了三件事情: 1. UI布局(addUI), 2. 数据绑定(bind) 3. 和上层交互(fetchData, onClickIconCommand) 相对而言, MVCVM中的UserInfoView作的事情就少多了, 只作了一件事情: UI布局. 不过它不只布了局, 还将对应的View也暴露了出来. 这些暴露出来的东西给谁用呢? 还有, 数据绑定和上层交互如今由谁来作呢? 显然只能是这个多出来的C层了, 看看这部分的代码吧:
@interface UserInfoController : NSObject
+ (instancetype)instanceWithView:(UserInfoView *)view viewModel:(UserInfoViewModel *)viewModel;
- (UserInfoView *)view;
- (void)fetchData;
- (void)setOnClickIconCommand:(RACCommand *)onClickIconCommand;
@end
复制代码
@interface UserInfoController ()
@property (strong, nonatomic) UserInfoView *view;
@property (strong, nonatomic) UserInfoViewModel *viewModel;
@property (strong, nonatomic) RACCommand *onClickIconCommand;
@end
@implementation UserInfoController
+ (instancetype)instanceWithView:(UserInfoView *)view viewModel:(UserInfoViewModel *)viewModel {
if (view == nil || viewModel == nil) { return nil; }
return [[UserInfoController alloc] initWithView:view viewModel:viewModel];
}
- (instancetype)initWithView:(UserInfoView *)view viewModel:(UserInfoViewModel *)viewModel {
if (self = [super init]) {
self.view = view;
self.viewModel = viewModel;
[self bind];
}
return self;
}
- (void)bind {
RAC(self.view.nameLabel, text) = RACObserve(self, viewModel.name);
RAC(self.view.summaryLabel, text) = RACObserve(self, viewModel.summary);
RAC(self.view.blogCountLabel, text) = RACObserve(self, viewModel.blogCount);
RAC(self.view.friendCountLabel, text) = RACObserve(self, viewModel.friendCount);
@weakify(self);
[RACObserve(self, viewModel.icon) subscribeNext:^(UIImage *icon) {
@strongify(self);
[self.view.iconButton setImage:icon forState:UIControlStateNormal];
}];
[[self.view.iconButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
@strongify(self);
[self.onClickIconCommand execute:self.viewModel.user];
}];
}
- (void)fetchData {
[[[self.viewModel fetchUserInfoCommand] execute:nil] subscribeError:^(NSError *error) {
//show error view
} completed:^{
//do completed
}];
}
@end
复制代码
代码一亮出来, 相信各位应该很清楚MVP/MVVM和MVCP/MVCVM的区别何在了, 简单描述一下就是是否拆分UI布局和数据绑定(注意: 是数据绑定, 不是业务逻辑, 业务逻辑都在VM层).
毫无疑问, 拆分更加细致的MVCVM比MVVM要好一些, 纯布局的V层优势在MVC部分已经介绍过了, 复用性贼好, 另外, 布局拆出来之后, 数据绑定层的代码看起来会更加简洁, 易读性也很好. 然而, 最初的demo里面并无包含这种写法的例子, 这算是我本身的缘由. 由于实际开发一般没有这么细粒度的复用模块(UI和产品不给机会), 另外我本人习惯用xib/sb作页面布局, 因此V层也不会有什么布局代码, 长此以往, 本身写的代码都是MVVM而不是MVCVM, 习惯成天然了.
Q: V层直接声明了P/VM的属性, 数据绑定又是写死的, 那不就是一对一了, 怎么复用呢?
A: 注意到我描述P/VM层时都是说: xxxP/VM.h暴露了那些接口, 而不是xxxP/VM有那些属性. 换句话说, P/VM其实只是定义了一套规范, 可是这套规范的实现倒是千差万别的, 当只有一个实现时确实是一对一的, 当有多个实现时就是一对多了. 举个我项目中的例子吧, 我有好友列表, 关注列表, 用户列表三个不一样数据源不一样数据操做的列表, 但这三张表cell的布局展现倒是如出一辙的, 只是展现的文字不同, 点击按钮有的是加/取消好友, 有的是加/取消关注, 这就是典型的布局不变可是逻辑变化的例子, 因此我只写了一个cell, 一个cellViewModel接口, 可是viewModel的接口实现倒是两套, 对应到代码中:
//HHUserCellViewModel.h
@interface HHUserCellViewModel : NSObject
+ (instancetype)friendCellViewModelWithUser:(HHUser *)user;
+ (instancetype)followCellViewModelWithUser:(HHFriend *)user;
- (id)user;
- (BOOL)isVip;
- (NSURL *)userAvatarURL;
- (NSString *)userName;
- (NSString *)userSignature;
- (NSString *)userFriendCount;
- (NSString *)rightButtonTitle;
- (NSString *)rightButtonEventName;
- (RACCommand *)rightButtonCommand;
- (BOOL)deleteButtonHidden;
- (UIImage *)deleteButtonImage;
- (RACCommand *)deleteButtonCommand;
- (CGFloat)contentHeight;
@end
复制代码
//HHUserCellViewModel基类: 这里定义了两套实现都会用到的属性和方法
@interface HHUserCellViewModel ()
@property (strong, nonatomic) HHUser *user;
@property (copy, nonatomic) NSString *rightButtonTitle;
@property (strong, nonatomic) RACCommand *rightButtonCommand;
@property (assign, nonatomic) BOOL deleteButtonHidden;
@property (strong, nonatomic) UIImage *deleteButtonImage;
@property (strong, nonatomic) RACCommand *deleteButtonCommand;
@end
#pragma mark - HHFollowCellViewModel
//HHFollowCellViewModel子类: 关注模式的viewModel的实现
@interface HHFollowCellViewModel : HHUserCellViewModel
@end
@implementation HHFollowCellViewModel
- (instancetype)initWithUser:(HHFriend *)user {
if (self = [super initWithUser:user]) {
[self switchRightButtonColor:user.followState];
@weakify(self);
self.rightButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
if ([self.rightButtonTitle isEqualToString:@"已关注"]) {
self.deleteButtonHidden = !self.deleteButtonHidden;
return [RACSignal empty];
} else {
[self switchRightButtonColor:YES];
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//点击右侧按钮调用加关注接口
[[HHSocketFollowAPIManager new] followWithFollowUser:self.user completionHandler:^(NSError *error, id result) {
if (error) {
[self switchRightButtonColor:NO];
}
if ([USER_ID integerValue] != 0) {
[subscriber sendNext:@(error == nil)];
}
[subscriber sendCompleted];
}];
return nil;
}];
}
}];
self.deleteButtonImage = [UIImage imageNamed:@"unfollow.png"];
self.deleteButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//点击删除按钮调用取消关注接口
self.deleteButtonHidden = YES;
[[HHSocketFollowAPIManager new] unfollowWithUnfollowUser:self.user completionHandler:^(NSError *error, id result) {
if (error) {
[subscriber sendError:error];
} else {
[self switchRightButtonColor:NO];
[subscriber sendCompleted];
}
}];
return nil;
}];
}];
}
return self;
}
- (void)switchRightButtonColor:(BOOL)isSelected {
[super switchRightButtonColor:isSelected];
self.rightButtonTitle = isSelected ? @"已关注" : @"+关注";
}
- (CGFloat)contentHeight {
return 68;
}
@end
#pragma mark - HHFriendCellViewModel
//HHFriendCellViewModel子类: 好友模式的viewModel的实现
@interface HHFriendCellViewModel : HHUserCellViewModel
@end
@implementation HHFriendCellViewModel
- (instancetype)initWithUser:(HHFriend *)user {
if (self = [super initWithUser:user]) {
[self switchRightButtonColorWithFriendState:self.user.friendState];
@weakify(self);
self.rightButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
if ([self.rightButtonTitle isEqualToString:@"加好友"]) {
[self switchRightButtonColorWithFriendState:1];
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//点击右侧按钮调用加好友接口
[[HHSocketFriendAPIManager new] addFriendWithUser:self.user msg:@"你好, 我是xxx" completionHandler:^(NSError *error, id result) {
if (error) {
[self switchRightButtonColorWithFriendState:0];
}
if ([USER_ID integerValue] != 0) {
[subscriber sendNext:@(error == nil)];
}
[subscriber sendCompleted];
}];
return nil;
}];
} else if([self.rightButtonTitle isEqualToString:@"好友"]) {
self.deleteButtonHidden = !self.deleteButtonHidden;
}
return [RACSignal empty];
}];
self.deleteButtonImage = [UIImage imageNamed:@"deleteFriend.png"];
self.deleteButtonCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
self.deleteButtonHidden = !self.deleteButtonHidden;
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//点击删除按钮调用删除好友接口
[[HHSocketFriendAPIManager new] deleteFriendWithUser:self.user completionHandler:^(NSError *error, id result) {
if (error) {
[subscriber sendError:error];
} else {
[self switchRightButtonColorWithFriendState:0];
[subscriber sendCompleted];
}
}];
return nil;
}];
}];
}
return self;
}
- (void)switchRightButtonColorWithFriendState:(NSInteger)state {
self.user.friendState = state;
switch (state) {
case 0: {
[super switchRightButtonColor:NO];
self.rightButtonTitle = @"加好友";
} break;
case 1: {
self.rightButtonTitleColor = kColorGrayNine;
self.rightButtonBorderColor = self.rightButtonBackgroundColor = [UIColor whiteColor];
self.rightButtonTitle = @"验证中";
} break;
case 2: {
[super switchRightButtonColor:YES];
self.rightButtonTitle = @"好友";
} break;
}
}
- (CGFloat)contentHeight {
return self.user.userId != [USER_ID integerValue] && self.user.commonFriendCount > 0 ? 91 : 68;
}
@end
#pragma mark - HHUserCellViewModel
@implementation HHUserCellViewModel
+ (instancetype)friendCellViewModelWithUser:(HHFriend *)user {
return [[HHFriendCellViewModel alloc] initWithUser:user];
}
+ (instancetype)followCellViewModelWithUser:(HHFriend *)user {
return [[HHFollowCellViewModel alloc] initWithUser:user];
}
//HHUserCellViewModel基类: 一些实现相同的接口直接在此处实现 省得重复如出一辙的代码
#pragma mark - PublicInterface
- (BOOL)isVip {
return self.user.level > 0;
}
- (NSString *)userName {
return self.user.nickname;
}
- (NSString *)userFriendCount {
return self.user.commonFriendCount > 0 ? [NSString stringWithFormat:@"大家有%ld个共同好友", self.user.commonFriendCount] : @"";
}
- (NSString *)userSignature {
return self.user.signature.length > 0 ? self.user.signature : @"TA很懒,什么都没写";
}
- (NSURL *)userAvatarURL {
return self.user.avatar.HHUrl;
}
复制代码
对于Cell而言, 它只知道本身该怎么样布局, 本身会有一个实现了HHUserCellViewModel接口的属性, 而后会去绑定这些接口的数据进行展现, 点击之后调用哪一个Command, 至于具体展现出来的是好友仍是关注, 点击具体会执行什么事件, 它彻底不关心, 它只管绑定, 其余的事情上层会处理好的.
这里也是出于我的习惯, 我本人特别喜欢用类簇或者说抽象工厂, 由于这样能少建不少文件, 一个类就能作完全部事情. 如今想来, 若是一开始demo里面写的就是Protocol, 而后多个类实现这个Protocol可能就不会有人有疑问了.
额... 原本有好几个问题的, 可是简书有字数限制(代码好像也算字?), 我又不太想另开一遍啰嗦啰嗦, 只能选择把相对重要的MVVM这部分放出来了, 望海涵