MVVM模式的簡單通俗理解

VanBurford 7年前發布 | 51K 次閱讀 MVVM模式 iOS開發 移動開發

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

 

 本文由用戶 VanBurford 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!