MVVM模式的簡單通俗理解

目前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:


Paste_Image.png

下面是代碼示例:

@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也未嘗不可斟珊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市富纸,隨后出現(xiàn)的幾起案子囤踩,更是在濱河造成了極大的恐慌,老刑警劉巖晓褪,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堵漱,死亡現(xiàn)場離奇詭異,居然都是意外死亡涣仿,警方通過查閱死者的電腦和手機勤庐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來好港,“玉大人愉镰,你說我怎么就攤上這事【冢” “怎么了丈探?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拔莱。 經(jīng)常有香客問我碗降,道長隘竭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任讼渊,我火速辦了婚禮动看,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘爪幻。我一直安慰自己弧圆,他們只是感情好,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布笔咽。 她就那樣靜靜地躺著,像睡著了一般霹期。 火紅的嫁衣襯著肌膚如雪叶组。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天历造,我揣著相機與錄音甩十,去河邊找鬼。 笑死吭产,一個胖子當著我的面吹牛侣监,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播臣淤,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼橄霉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了邑蒋?” 一聲冷哼從身側(cè)響起姓蜂,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎医吊,沒想到半個月后钱慢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡卿堂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年束莫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片草描。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡览绿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出陶珠,到底是詐尸還是另有隱情挟裂,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布揍诽,位于F島的核電站诀蓉,受9級特大地震影響栗竖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜渠啤,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一狐肢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沥曹,春花似錦份名、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至壶栋,卻和暖如春辰如,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贵试。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工琉兜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人毙玻。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓豌蟋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親桑滩。 傳聞我的和親對象是個殘疾皇子梧疲,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容