KVO實現MVVM

文章出處

為什么要用MVVM替代MVC

Apple倡導開發者們使用MVC模式開發App程序,但很多人都沒有嚴格按照MVC的模式去開發,只是讓程序的架構看上去像MVC,而實際上是MC或VC。

很多入門開發者都有一個通病,就是把所有的邏輯,界面生成都寫進ViewController中,這樣ViewController就變成了一個Massive View Controller(重量級視圖控制器)。重量級視圖控制器會讓整個ViewController變得非常復雜且不可維護,讓維護者崩潰卻無從下手,只能忍痛默默的重寫整個邏輯。

但是,有一種解決方案,可以解決Massive View Controller的問題,那就是MVVM。

這是傳統MVC模式:

這是MVVM模式:

很多時候新手們會把數據轉換邏輯,網絡請求邏輯等都放到ViewController中,這樣會不可避免的讓ViewController變得臃腫,這是造成重量級視圖控制器的重要原因。

除了上面提到的一個原因外,由于AFNetworking是iOS開發網絡訪問框架的事實標準,而AFNetworking使用的是block來實現網絡回調,block會讓block里面引用的變量的引用數+1,在某種網速非常緩慢的極端情況下,當用戶打開ViewController的時候,網絡請求已經發出,也就是說在block中的變量引用已經+1,如果此時用戶退出這個ViewController,當這個block發生回調時,此時持有block里面變量的ViewController已經被回收,而block里面的變量由于block的原因沒有被及時回收,這樣會造成crash的。這是在使用block進行回調時很容易被忽略的情況。

對于業務而言,將所有業務不加區分都放進ViewController中,這顯然是一種懶惰的表現。一個ViewController中包含大量業務的細節,將使這個ViewController在業務協調和調用中迷失,將讓這個業務變得非常混亂,讓業務邏輯變得無法維護。由于重量級ViewController的復雜性,其代碼將難以復用。

以上原因是傳統MVC難以解決的,為解決這些問題,要采用MVVM的開發模式。

MVVM不是什么新鮮事,簡單說就是將部分邏輯從ViewController中拆分出來,并整合起來在ViewController和Model中間加多一個ViewModel,ViewModel不直接引用View,ViewController也不引用Model中的方法,所有網絡回調數據處理等邏輯都放到ViewModel中,ViewController通過ViewModel來請求數據和更新數據。

如何實踐MVVM

參考項目:https://github.com/britzlieg/MVVMDemo/tree/master

第一步:創建Model

AFNetworking請求方法放到Model中

@interface Model : NSObject

@property (nonatomic, copy) NSString *col;

@property (nonatomic, copy) NSString *sort;

@property (nonatomic, copy) NSString *tag3;

@property (nonatomic, assign) NSInteger startIndex;

@property (nonatomic, assign) NSInteger returnNumber;

@property (nonatomic, strong) NSArray *imgs;

@property (nonatomic, copy) NSString *tag;

@property (nonatomic, assign) NSInteger totalNum;

+ (void)getImagesListWithPage: (NSInteger)aPage SuccessBlock :(SuccessBlock)success FailBlock :(FailBlock)fail;

具體實現,不多說:

@implementation Model
+ (void)getImagesListWithPage: (NSInteger)aPage SuccessBlock :(SuccessBlock)success FailBlock :(FailBlock)fail {

    NSString *urlString = [NSString stringWithFormat:@"%@%ld%@",
    @"http://image.baidu.com/data/imgs?col=%e7%be%8e%e5%a5%b3&tag=%e5%b0%8f%e6%b8%85%e6%96%b0&sort=0&pn=1",
    aPage,@"&rn=1&p=channel&from=1"];
    AFHTTPRequestOperationManager *managere = [AFHTTPRequestOperationManager manager];
    [managere GET:urlString parameters:nil 
    success:^(AFHTTPRequestOperation * _Nonnull operation, id  _Nonnull responseObject) {
        success(responseObject,nil);
        NSLog(@"success");
    } failure:^(AFHTTPRequestOperation * _Nullable operation, NSError * _Nonnull error) {
        fail(nil,error);
        NSLog(@"fail");
    }];
}
@end

第二步:創建ViewModel

ViewModel的屬性:

  • data : 請求獲取的數據
  • racMsg : 請求成功和失敗的信號量(主要用KVO對這個進行監視)
@interface ViewModel : NSObject

@property (strong,nonatomic) NSDictionary *data;
@property (strong,nonatomic) NSString *racMsg;  


- (void)getImagesList;
- (void)getNextImagesList;
- (void)getPreImagesList;

@end

ViewController中主要監視ViewModel的racMsg來發現data更新。

#define WS(weakSelf)  __weak __typeof(&*self)weakSelf = self;

@interface ViewModel()
@property (nonatomic) NSInteger currentPage;
@end

@implementation ViewModel

- (instancetype)init {
    self = [super init];
    self.currentPage = 0;
    return self;
}

- (void)getImagesList {
    WS(ws)
    [Model getImagesListWithPage:0 
    SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
        ws.data = responseObjectDict;
        ws.racMsg = @"success";
    } FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
        ws.data = nil;
        ws.racMsg = @"fail";

    }];
}

- (void)getNextImagesList {
    WS(ws)
    self.currentPage++;
    [Model getImagesListWithPage:self.currentPage 
    SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
        ws.data = responseObjectDict;
        ws.racMsg = @"success";
    } FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
        ws.data = nil;
        ws.racMsg = @"fail";
    }];
}

- (void)getPreImagesList {
    WS(ws)
    self.currentPage = self.currentPage == 0 ? 0 : self.currentPage-1;
    [Model getImagesListWithPage:self.currentPage 
    SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
        ws.data = responseObjectDict;
        ws.racMsg = @"success";
    } FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
        ws.data = nil;
        ws.racMsg = @"fail";
    }];
}



@end

可能會有人覺得為什么不直接監視data,但我個人更傾向于采用一種類似于信號量的機制,監聽特定的信號來更新數據。如果直接監視data,則data只有nil和非nil兩種情況,要進一步區分請求的狀態的話必須要對data進行解析,增加了轉換成本,不如直接采用多一個屬性變量進行判斷和協調。

第三步:ViewController中KVO設置

ViewController直接持有viewModel

@interface ViewController ()

@property (strong,nonatomic) ViewModel *viewModel;

@property (strong,nonatomic) UITextView *showTextView;

@end

加載ViewController時初始化KVO和調用ViewModel方法getImagesList來請求數據

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // requestData
    [self _initViews];
    [self setupKVO];
    [self.viewModel getImagesList];

}

ViewController銷毀時去除KVO

- (void)dealloc {
    [self removeKVO];
}

KVO相關的函數。observeValueForKeyPath只需對racMsg進行判斷就可以知道data的值是否更新了,如果更新了就更新一下View。

#pragma mark - KVO
- (void)setupKVO {
    [self.viewModel addObserver:self 
    forKeyPath:@"racMsg" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil];
}

- (void)removeKVO {
    [self.viewModel removeObserver:self forKeyPath:@"racMsg"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
         change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"racMsg"]) {
        if ([_viewModel.racMsg isEqualToString:@"success"]) {
            _showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
        }
        else {
            _showTextView.text = @"error";
        }
    }
}

按鈕點擊事件和View的初始化

#pragma mark - Event Response
- (void)getPre {
    [self.viewModel getPreImagesList];
}

- (void)getNext {
    [self.viewModel getNextImagesList];
}

#pragma mark - Private
- (void)_initViews {
    UIButton *preBtn = [[UIButton alloc]initWithFrame:CGRectMake(20, 50, 200, 40)];
    [preBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    [preBtn setTitle:@"Pre" forState:UIControlStateNormal];
    [preBtn addTarget:self action:@selector(getPre) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:preBtn];

    UIButton *nextBtn = [[UIButton alloc]initWithFrame:CGRectMake(20, 150, 200, 40)];
    [nextBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
    [nextBtn setTitle:@"nextBtn" forState:UIControlStateNormal];
    [nextBtn addTarget:self action:@selector(getNext) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:nextBtn];

    _showTextView = [[UITextView alloc]initWithFrame:CGRectMake(0, 200, 320, 200)];
    _showTextView.backgroundColor = [UIColor lightGrayColor];
    [self.view addSubview:_showTextView];

}

實踐MVVM的具體好處的例子

下面討論一下用MVVM的具體好處的例子。

case 1:ViewController需要一個額外請求一個文章列表,這個文章列表的請求參數與當前請求圖片列表的接口返回結果沒有任何關聯,可以并行請求。

在這種情況下,在ViewModel中加入方法getArticleList()和屬性articleList以及articleMsg,然后在ViewController中需要調用該方法的位置調用該方法即可。KVO中的observeValueForKeyPath方法稍微修改一下:


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
         change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"racMsg"]) {
        if ([_viewModel.racMsg isEqualToString:@"success"]) {
            _showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
        }
        else {
            _showTextView.text = @"error";
        }
    }
    else if([keyPath isEqualToString:@"articleMsg"]) {
        if ([_viewModel.articleMsg isEqualToString:@"success"]) {
            _articleTextView.text = _viewModel.articleList
        }
        else {
            _articleTextView.text = @"error";
        }
    }
}

可見并行業務功能上的擴展是非常簡單的,整個Controller的總體邏輯幾乎不用怎么變化,只需要改動局部細節即可。

case 2:同case 1,但是文章列表的請求參數需要通過圖片列表接口返回的結果獲取,請求是串聯嵌套的(即先請求圖片列表接口,請求完成后,根據返回結果再請求文章列表接口)。

對于嵌套的請求,如果采用傳統MVC模式,就要在ViewController中加入兩個block,一個嵌套另外一個,這樣會讓代碼變得非常難看,而且會讓子block依賴于父block,難以對其進行拆分。但如果采用MVVM,則會將所有的請求變化都置于KVO的監控之下,并作出統一的處理。

ViewModel中的與case 1一樣,但ViewController中的處理稍微不同。


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
         change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"racMsg"]) {
        if ([_viewModel.racMsg isEqualToString:@"success"]) {
            _showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
            // 請求文章
            [_viewModel getArticleList];
        }
        else {
            _showTextView.text = @"error";
        }
    }
    else if([keyPath isEqualToString:@"articleMsg"]) {
        if ([_viewModel.articleMsg isEqualToString:@"success"]) {
            _articleTextView.text = _viewModel.articleList
        }
        else {
            _articleTextView.text = @"error";
        }
    }
}

可以看出還是不需要改動大邏輯,即可對有依賴的業務進行擴展。

case 3: 同case 2,但是增加一個依賴于文章列表返回結果的評估列表接口(即圖片->文章->評論)。

假設存在一種這樣的情況,請求數據的順序是:圖片->文章->評論,這樣的話如果用傳統的MVC模式做的,就是三層block的嵌套,這對于一個ViewController來說是噩夢。三層嵌套,意味著無法復用,只能寫死在這個ViewController之中。這時使用MVVM就顯得非常必要了。

當出現三層以上的依賴時,其實可以考慮將所有依賴捆綁在一起,做成一個高內聚的模塊,在MVVM中,由于請求數據的方法不是寫在ViewController之中,所以將這三個模塊進行內聚的工作是放到ViewModel之中。

ViewModel.m :


- (void)getCommentsList {
    WS(ws)
    [Model getImagesListWithPage:0 
    SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
        ws.data = responseObjectDict;
        ws.racMsg = @"success";
            [Model getArticlesListWithPage:0 
                SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
                  ws.article = responseObjectDict;
                  ws.articleMsg = @"success";
                    [Model getCommentsListWithPage:0 
                        SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
                         ws.comments = responseObjectDict;
                         ws.commentsMsg = @"success";  // 成功
                        } FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
                            ws.comments = nil;
                            ws.commentsMsg = @"fail";  // 失敗
                         }];
                
                 } FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
                    ws.article = nil;
                    ws.articleMsg = @"fail";
                 }];
        
    } FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
        ws.data = nil;
        ws.racMsg = @"fail";
    }];
}

ViewController.h中:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
         change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"racMsg"]) {
        if ([_viewModel.racMsg isEqualToString:@"success"]) {
            _showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
            // 請求文章
            [_viewModel getArticleList];
        }
        else {
            _showTextView.text = @"error";
        }
    }
    else if([keyPath isEqualToString:@"articleMsg"]) {
        if ([_viewModel.articleMsg isEqualToString:@"success"]) {
            _articleTextView.text = _viewModel.articleList
        }
        else {
            _articleTextView.text = @"error";
        }
    }
    else if([keyPath isEqualToString:@"commentsMsg"]) {
        if ([_viewModel.commentsMsg isEqualToString:@"success"]) {
            _commentsTextView.text = _viewModel.commentsList
        }
        else {
            _commentsTextView.text = @"error";
        }
    }
}

三層嵌套的block是無法避免的,但是MVVM可以將這個惡心的東西放到ViewModel中,而不是ViewController中,這樣當ViewController要獲取評論時,只需要調用viewModel的getCommentsList即可,不需要看到三層block請求的細節,這樣可以很好的將邏輯與細節隔離。

可以看出使用MVVM還是能夠很方便的擴展多層依賴的業務。

case 4: 另外一個ViewController需要調用圖片接口獲取數據

這種情況非常簡單,直接在ViewController2中加入一個ViewModel的屬性,其他按照ViewController中的調用方式調用即可。

由于將請求邏輯放到了ViewController,所以ViewModel對ViewController是沒有依賴的,所以ViewController2能夠很好的直接使用ViewModel,這是傳統MVC很難做得到的。

case 5: 另外一個ViewController需要評論接口來獲取數據(將三個接口請求過程內聚,便于其他復用)

這種情況也非常簡單,與case 4是一樣的,直接調用ViewModel中的getCommentsList(),而這個函數是已經在ViewModel中高內聚了,所以使用也非常方便,代碼復用和請求邏輯復用都非常方便清晰!

case 6: ViewController中有很多數據轉換邏輯,多個ViewController的數據轉換邏輯都相同的情況。

ViewModel中不僅只包含請求邏輯,還可以包含數據轉換的邏輯,還有一些不知要怎么歸類的雜七雜八的邏輯。多個ViewController發生相同的數據轉換的情況是經常會有的,如果把數據轉換邏輯寫到Controller之中,會讓每一個Controller都持有一個轉換邏輯,這對于數據轉換邏輯的統一來說是非常糟糕的。如果把相同的數據轉換邏輯都抽象封裝到同一個ViewModel中,ViewController不直接持有數據轉換邏輯,而是通過ViewModel來調用的話,每個ViewController只需要維護一個ViewModel實例即可,所有轉換細節都可以在ViewModel中進行統一修改。Controller只關心數據和數據與View的交互,不應該關心數據之間的轉換和數據怎樣獲取的,這應該是MVVM的一個原則。

總結

MVVM的核心在于綁定,本文采用的是KVO的綁定機制,能夠很好與Objective-C和Cocoa結合起來,不需要借用第三方的類庫進行數據綁定。
除了使用KVO,業界通常采用的是ReactiveCocoa。但是,ReactiveCocoa的學習成本過高,不適合輕量級的開發,而MVVM只是一種開發模式,并不是一種具體的框架,所以如果不是非常想深入使用MVVM的精髓的話,是沒有必要去學習ReactiveCocoa的。網上還有一些討論MVVM的博客提到ViewModel直接對View進行操作,其實這是一種很不嚴謹的做法,MVVM中的ViewModel不應該關心View的顯示,只應該關心數據的獲取和轉換,View如何顯示那是ViewController的職責。所以凡是在ViewModel中引用了UIKit的,個人認為都不是一種嚴格意義上的MVVM。

當然MVVM也有其自身的不足,比如引入ViewModel之后,文件數量增加了不少,總的代碼量其實也會增加,這對于極簡主義者來說并不是一種很好的模式。而且MVVM的開發思路與MVC是不同的,開發者要轉換思路采用MVVM的開發方式其實還是有不少的學習成本,而且對于大部分簡單業務來說,使用MVVM會增加業務的復雜度,顯得臃腫和多余。本文中使用KVO的MVVM模式,從本質上來說其實是MVC的衍生,把C中的一部分拆分出來并隔離M和C,所以從模式上來說,這樣是完全可以兼容傳統的MVC開發模式的。因此,對于簡單的業務,可以直接采用MVC的模式開發,不需要額外創建一個ViewModel。對于復雜業務,就采用KVO的MVVM模式,進行業務拆分和復用。這種折中的方法能夠將MVC和MVVM的優點都利用起來,避免只使用一個造成開發效率上的降低。

MVVM不應該被誤解和神化,使用MVVM只是提供多了一個不錯的選擇,要不要使用它,還是要看具體的項目而定。但是用上了,就停不下來了。

參考文章

Model-View-ViewModel for iOS

MVVM 介紹

被誤解的MVC和被神化的MVVM

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容