從簡書遷移到掘金...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這部分放出來了, 望海涵