目前MVVM模式是移動開發(fā)里面討論的較多的開發(fā)設(shè)計模式了,隨之而來的還有ReactiveCocoa框架哩俭。但是MVVM設(shè)計模式并不意味著非要用ReactiveCocoa框架翻具,畢竟這個框架是一個重型框架弦撩,一般的應用也不用搞得這么復雜。前些時公司app改版侣诺,使用MVVM模式重構(gòu)了一下代碼,這里寫下來僅僅是記錄我這一段時間的實踐總結(jié)氧秘,希望能盡量說明白一點年鸳。
1、MVVM和MVC的區(qū)別
MVC不用說了丸相,都清楚搔确。MVVM的話,所有講MVVM的文章都會拿出這個圖:
與MVC的區(qū)別在于中間多了個View Model灭忠,以前的MVC是view controller直接和model打交道膳算,然后用model去填充view。這里MVVM的view model把view controller/view和model隔開了弛作。理論就說道這里涕蜂,那么問題是:
1、這樣做的好處是什么缆蝉?
2宇葱、怎么設(shè)計這個view model?
2刊头、MVC我們是怎么寫代碼的黍瞧?
比如這個普通的評論列表:
這個評論列表有三個地方要注意:一是動態(tài)行高,二是點贊數(shù)根據(jù)數(shù)量大小有不同的顯示原杂,三是回復評論前面要加上顏色不同的“@XX”印颤。一般MVC寫代碼是這樣的,代碼結(jié)構(gòu)如下:
下面是主要代碼:
@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穿肄、獲取數(shù)據(jù)
- (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咸产、根據(jù)點贊數(shù)量顯示“改造”后的點贊數(shù)量的邏輯
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)打交道的,對于數(shù)據(jù)的處理邏輯脑溢,也是直接寫在view controller和view中的僵朗,比如:
1、獲取數(shù)據(jù):像上面的標注1處,如果這個地方的邏輯變得復雜验庙,比如有緩存數(shù)據(jù)顶吮,先要讀取數(shù)據(jù)庫,判斷有沒有緩存數(shù)據(jù)粪薛,沒有的話請求網(wǎng)絡(luò)悴了,數(shù)據(jù)回來之后還要解析,存儲违寿,那么1處的代碼會變得冗長湃交。
2、行高計算:很多應用都涉及到動態(tài)行高計算陨界,像標注2處寫在這里首先是讓view controller臃腫巡揍,另外這個行高方法會頻繁調(diào)用,那么頻繁計算會嚴重影響tableView的滑動性能菌瘪。
3腮敌、數(shù)據(jù)加工邏輯:有些model的屬性是不能直接為view所用的,比如上面3俏扩、4兩處需要將model的屬性加工一下再顯示糜工,MVC中這個加工邏輯也是寫在view中的。
這只是一個簡單的例子录淡,簡單的例子這樣寫沒有什么大問題捌木。但是如果遇到比較復雜的界面,這么寫下去會導致view controller和view的代碼越來越多嫉戚,而且難以復用刨裆,MVC就變成了胖view controller模式。
3彬檀、MVVM怎么寫帆啃?
MVVM的提出就是為了減輕view controller和view的負擔的,view model將上面提到的獲取數(shù)據(jù)窍帝,行高計算努潘,數(shù)據(jù)加工邏輯從view controller和view中剝離出來,同時把view controller/view和model隔離開坤学。
3.1疯坤、剝離行高計算,數(shù)據(jù)加工邏輯
如下所示深浮,添加view model:
下面是代碼示例:
@interface KTCommentViewModel : NSObject
@property (nonatomic, strong) KTComment *comment;
// 根據(jù)文本多少計算得到行高
@property (nonatomic, assign) CGFloat cellHeight;
// 根據(jù)是否是回復压怠,計算得到的富文本
@property (nonatomic, copy) NSAttributedString *commentContent;
// 根據(jù)點贊數(shù)計算得到的顯示文字
@property (nonatomic, copy) NSString *favourString;
@end
@implementation KTCommentViewModel
- (void)setComment:(KTComment *)comment
{
_comment = comment;
// 1、計算行高飞苇,并用屬性存起來
// 2刑峡、根據(jù)是否是回復洋闽,計算得到的富文本
// 3、根據(jù)點贊數(shù)計算得到的顯示文字
}
@end
這里的1突梦、2、3處的代碼基本上等同于將前面view羽利、view controller中2宫患、3、4處的代碼拷貝過來这弧,這里就省略了娃闲。可以看出view model的作用是:
1匾浪、和model打交道皇帮。
2、做一些邏輯處理和計算蛋辈。
3属拾、和view、view controller打交道冷溶,并提供更為直觀的數(shù)據(jù)渐白,比如上面的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泉唁、剝離獲取數(shù)據(jù)邏輯
如下創(chuàng)建一個列表view model:
代碼示例如下:
@interface KTCommentListViewModel : NSObject
@property (nonatomic, copy) NSArray<KTCommentViewModel *> *commentViewModelList;
- (void)loadComments;
@end
KTCommentListViewModel的職責也很清楚鹅龄,就是負責獲取數(shù)據(jù),然后為每個comment創(chuàng)建一個KTCommentViewModel對象亭畜,并保存到列表中扮休。那么view controller就可以將獲取數(shù)據(jù)的代碼挪到這個view model中來,view controller只用調(diào)用KTCommentListViewModel提供的方法和數(shù)據(jù)就可以了:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
self.commentListViewModel = [[KTCommentListViewModel alloc] init];
[self.commentListViewModel loadComments];
}
4拴鸵、總結(jié)
基本上算是搞懂了第一張圖的含義玷坠。view和view controller擁有view model蜗搔,view model擁有model,相比較MVC的區(qū)別在于view和view controller是通過view model來間接操作數(shù)據(jù)的八堡。這樣做的意義在于樟凄,對于一些比較復雜的操作邏輯,可以寫到view model里面兄渺,從而簡化view和view controller缝龄,view和view controller只干展示數(shù)據(jù)和接受交互事件就好了;反過來model的update挂谍,驅(qū)動view model的update叔壤,然后再驅(qū)動view和view controller變化,這個中間的加工邏輯也可以寫在view model中口叙。
當然對于一些比較簡單的應用界面炼绘,使用MVC就綽綽有余了,并不需要用MVVM妄田,用哪種
還要看實際情況和個人喜好吧俺亮。
另外如同 @Noah1985 說的,我這個例子并沒有加上model反向驅(qū)動view model和view/view controller的部分形庭,并不能算是完全的MVVM铅辞,實際應用中可以加上RAC。但如果自己能理清回調(diào)和update機制的話萨醒,不用RAC也未嘗不可斟珊。