MVVM模式的簡單通俗理解
目前MVVM模式是移動開發里面討論的較多的開發設計模式了,隨之而來的還有ReactiveCocoa框架。但是MVVM設計模式并不意味著非要用ReactiveCocoa框架,畢竟這個框架是一個重型框架,一般的應用也不用搞得這么復雜。前些時公司app改版,使用MVVM模式重構了一下代碼,這里寫下來僅僅是記錄我這一段時間的實踐總結,希望能盡量說明白一點。
1、MVVM和MVC的區別
MVC不用說了,都清楚。MVVM的話,所有講MVVM的文章都會拿出這個圖:
與MVC的區別在于中間多了個View Model,以前的MVC是view controller直接和model打交道,然后用model去填充view。這里MVVM的view model把view controller/view和model隔開了。理論就說道這里,那么問題是:
1、這樣做的好處是什么?
2、怎么設計這個view model?
2、MVC我們是怎么寫代碼的?
比如這個普通的評論列表:
這個評論列表有三個地方要注意:一是動態行高,二是點贊數根據數量大小有不同的顯示,三是回復評論前面要加上顏色不同的“@XX”。一般MVC寫代碼是這樣的,代碼結構如下:
下面是主要代碼:
@implementation KTCommentsViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
self.title = @"評論列表";
[self.tableView registerNib:[UINib nibWithNibName:@"KTCommentCell" bundle:nil] forCellReuseIdentifier:kKTCommentCellIdentifier];
[self createData];
}
// 1、獲取數據
- (void)createData
{
NSMutableArray *array = [NSMutableArray arrayWithCapacity:10];
for (NSUInteger ii = 0; ii < 20; ++ii) {
KTComment *comment = [[KTComment alloc] init];
comment.commentId = ii + 1;
[array addObject:comment];
comment.userName = [NSString stringWithFormat:@"名字%lu", (unsigned long)(ii + 1)];
comment.userAvatar = @"user_default";
NSMutableArray *strsArray = [NSMutableArray arrayWithCapacity:ii + 1];
for (NSUInteger jj = 0; jj < ii + 1; ++jj) {
[strsArray addObject:@"這是評論"];
}
comment.content = [strsArray componentsJoinedByString:@","];
comment.commentTime = [NSDate date];
if (ii % 3 == 0) {
comment.repliedUserId = 10;
comment.repliedUserName = @"張三";
comment.favourNumber = 1000 * 3 * 10 * ii;
} else {
comment.favourNumber = 3000 * ii;
}
}
self.commentsList = array;
}
#pragma mark -- tableView --
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.commentsList.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 2、計算高度
KTComment *comment = [self.commentsList objectAtIndex:indexPath.row];
CGFloat width = [UIScreen mainScreen].bounds.size.width - 10 - 12 - 35 - 10;
CGFloat commnetHeight = [comment.content boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:14]} context:nil].size.height;
return commnetHeight + 15 + 21;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
KTCommentCell *cell = [tableView dequeueReusableCellWithIdentifier:kKTCommentCellIdentifier forIndexPath:indexPath];
KTComment *comment = [self.commentsList objectAtIndex:indexPath.row];
cell.comment = comment;
return cell;
}
// KTCommentCell
- (void)setComment:(KTComment *)comment
{
_comment = comment;
[_avatarImageView setImage:[UIImage imageNamed:comment.userAvatar]];
[_nameLabel setText:comment.userName];
[_timeLabel setText:[comment.commentTime ov_commonDescription]];
// 3、判斷是否是回復評論的邏輯
if (comment.repliedUserName.length > 0) {
NSMutableAttributedString *attrContent = [[NSMutableAttributedString alloc] init];
NSString *header = [NSString stringWithFormat:@"@%@ ", comment.repliedUserName];
NSAttributedString *reply = [[NSAttributedString alloc] initWithString:header attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:14], NSForegroundColorAttributeName : [UIColor blueColor]}];
[attrContent appendAttributedString:reply];
NSAttributedString *content = [[NSAttributedString alloc] initWithString:comment.content attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:14], NSForegroundColorAttributeName : [UIColor darkGrayColor]}];
[attrContent appendAttributedString:content];
[_commentLabel setAttributedText:attrContent];
} else {
[_commentLabel setText:comment.content];
}
// 4、根據點贊數量顯示“改造”后的點贊數量的邏輯
NSString *favourString = nil;
if (comment.favourNumber == 0) {
favourString = nil;
} else if (comment.favourNumber < 10000) {
favourString = [NSString stringWithFormat:@"%lld贊", comment.favourNumber];
} else if (comment.favourNumber < 10000) {
float floatNum = (double)comment.favourNumber / 10000.0;
favourString = [NSString stringWithFormat:@"%.1f萬贊", floatNum];
} else {
NSInteger intNum = comment.favourNumber / 10000;
favourString = [NSString stringWithFormat:@"%ld萬贊", (long)intNum];
}
_favourLabel.text = favourString;
}
MVC模式里面,可以看出我們的view controller和view(KTCommentCell)是直接和Model(KTComment)打交道的,對于數據的處理邏輯,也是直接寫在view controller和view中的,比如:
1、獲取數據:如上面的標注1處,如果這個地方的邏輯變得復雜,比如有緩存數據,先要讀取數據庫,然后判斷有沒有緩存數據,沒有的話請求網絡,數據回來之后還要解析,那么1處的代碼會變得冗長。
2、行高計算:很多應用都涉及到動態行高計算,像標注2處寫在這里首先是讓view controller臃腫,另外這個行高方法會頻繁調用,那么頻繁計算會嚴重影響tableView的滑動性能。
3、數據加工邏輯:有些model的屬性是不能直接為view所用的,比如上面3、4兩處需要將model的屬性加工一下再顯示,MVC中這個加工邏輯也是寫在view中的。
這只是一個簡單的例子,簡單的例子這樣些沒有什么大問題。但是如果遇到比較復雜的界面,這么寫下去會導致view controller和view的代碼越來越多,而且難以復用,MVC就變成了胖view controller模式。
3、MVVM怎么寫?
MVVM的提出就是為了減輕view controller和view的負擔的,view model將上面提到的獲取數據,行高計算,數據加工邏輯從view controller和view中剝離出來,同時把view controller/view和model隔開。
3.1、剝離行高計算,數據加工邏輯
如下所示,添加view model:
下面是代碼示例:
@interface KTCommentViewModel : NSObject
@property (nonatomic, strong) KTComment *comment;
// 根據文本多少計算得到行高
@property (nonatomic, assign) CGFloat cellHeight;
// 根據是否是回復,計算得到的富文本
@property (nonatomic, copy) NSAttributedString *commentContent;
// 根據點贊數計算得到的顯示文字
@property (nonatomic, copy) NSString *favourString;
@end
@implementation KTCommentViewModel
- (void)setComment:(KTComment *)comment
{
_comment = comment;
// 1、計算行高,并用屬性存起來
// 2、根據是否是回復,計算得到的富文本
// 3、根據點贊數計算得到的顯示文字
}
@end
這里的1、2、3處的代碼基本上等同于將前面view、view controller中2、3、4處的代碼拷貝過來,這里就省略了。可以看出view model的作用是:
1、和model打交道。
2、做一些邏輯處理和計算。
3、和view、view controller打交道,并提供更為直觀的數據,比如上面的行cellHeight,commentContent,favourString等屬性。
這樣一來,上面的2、3、4處的代碼被移到view model中了,view、view controller清爽了很多,而且職責更加分明,行高頻繁計算也避免了,因為行高被view model給緩存了,只計算一遍就行了。下面是view controller和view的變化:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
KTCommentViewModel *viewModel = [self.commentsList objectAtIndex:indexPath.row];
return viewModel.cellHeight;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
KTCommentCell *cell = [tableView dequeueReusableCellWithIdentifier:kKTCommentCellIdentifier forIndexPath:indexPath];
KTCommentViewModel *viewModel = [self.commentsList objectAtIndex:indexPath.row];
cell.commentViewModel = viewModel;
return cell;
}
// KTCommentCell
- (void)setCommentViewModel:(KTCommentViewModel *)commentViewModel
{
_commentViewModel = commentViewModel;
[_avatarImageView setImage:[UIImage imageNamed:commentViewModel.comment.userAvatar]];
[_nameLabel setText:commentViewModel.comment.userName];
[_timeLabel setText:[commentViewModel.comment.commentTime ov_commonDescription]];
_commentLabel.attributedText = commentViewModel.commentContent;
_favourLabel.text = commentViewModel.favourString;
}
3.2、剝離獲取數據邏輯
如下創建一個列表view model:
代碼示例如下:
@interface KTCommentListViewModel : NSObject
@property (nonatomic, copy) NSArray<KTCommentViewModel *> *commentViewModelList;
- (void)loadComments;
@end
KTCommentListViewModel的職責也很清楚,就是負責獲取數據,然后為每個comment創建一個KTCommentViewModel對象,并保存到列表中。那么view controller就可以將獲取數據的代碼挪到這個view model中來,view controller只用調用KTCommentListViewModel提供的方法和數據就可以了:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
self.commentListViewModel = [[KTCommentListViewModel alloc] init];
[self.commentListViewModel loadComments];
}
4、總結
基本上算是搞懂了第一張圖的含義。view和view controller擁有view model,view model擁有model,相比較MVC的區別在于view和view controller是通過view model來間接操作數據的。這樣做的意義在于,對于一些比較復雜的操作邏輯,可以寫到view model里面,從而簡化view和view controller,view和view controller只干展示數據和接受交互事件就好了;翻過model的update,驅動view model的update,然后再驅動view和view controller變化,這個中間的加工邏輯也可以寫在view model中。
當然對于一些比較簡單的應用界面,使用MVC就綽綽有余了,并不需要用MVVM,用哪種還要看實際情況和個人喜好吧。
來自:http://www.jianshu.com/p/aed9a3705991