在此工程中,本文將討論將MVC改造爲MVVM須要的一些基本方法,同時會適當穿插部分關於MVVM概念性的討論!本文最大的意義在於,提供了一種讀者能夠復現的方式,逐步引出從MVC向MVVM儘量平滑過渡的一種方案;此外,也是爲數很少的ReactiveCocoa實例文章之一.本文是MVVM系列文文章的第二篇,在閱讀以前,您可能須要先閱讀下第一篇文章: 寫給iOS小白的MVVM教程(一): 從MVC到MVVM之一個典型的MVC應用場景php
Apple自己的UIKit框架是爲MVC模式設計的,因此你在無形之中寫就的代碼其實就是MVC,並且你甚至會以爲代碼就應該這麼寫,不這麼寫還能怎麼寫?!MVVM因爲缺少框架級別的支持,因此在iOS的開發中一直彷佛是很雞肋式的存在.直到出現了ReactiveCocoa!它從框架界別支持MVVM模式,它讓你真切地感受到本身之前的代碼真的太亂了,它也讓你真正有興趣去嘗試下一些比較流行的編程模式,好比響應式,函數式,MVVM等.出於本身的實際項目須要,必須最低支持 iOS 7版本,因此在進行本文以前,先對 RAC(ReactiveCocoa的簡稱,後文同)做了一番研究.雖然官方文檔指明 3.0版本的RAC,最低支持的 是iOS 8.0,可是咱們依然能夠經過 CocoaPods 安裝 2.5版本的ReactiveCocoa來在本身的項目中使用,具體細節參見: ReactiveCocoa,最受歡迎的iOS函數響應式編程庫(2.5版),沒有之一!html
提到MVC,你如今能夠先本身回想一下本身寫過的程序,而後再往下看.react
M 指的是Model,數據模型,它能夠是一個系統自身的類型,好比字符串,數組等,也能夠是一個自定義的類型. 以上篇文章爲例,你能夠認爲 YFMVCPostListViewController 的 categoryName 屬性是一個Model,也能夠認爲 articles 屬性是Model.Model 就是那個用來存儲數據的東西.ios
V,指的是View,通俗點說,全部UIView及其子類都屬於V部分.git
C,指的就是UIViewController 及其子類.github
因此說, UIKit自身就是爲MVC模式設計的,而你就算不清除什麼是MVC,但你的代碼其實就是MVC模式.當你閱讀本身之前的代碼或者別人的代碼時,常常感受這個代碼寫的好亂(shi)啊,其實這真的不是本身或別人的鍋,這是MVC自己難以免甚至必然會出現的一個坑!因此,後來有人借鑑其餘語言,提出了MVVM模式,並躬身實踐!web
首先,MVVM,從概念說上來講,真的很好,很吸引人,即便你可能看不太懂,也感受很高大上的樣子!可是,當你真的去百度相關概念時,每每會很納悶,彷佛比我如今還麻煩,甚至開始懷疑,MVVM應該還只停留在理論階段吧!--NO,只是由於你沒有找到合適的文章,沒有找到合適的工具--ReactiveCocoa!仍是先說一下 MVVM的基礎概念吧,否則無法往下說了:編程
第一個M,和MVC中的M基本同樣.可是要求更輕量級.MVC中的M,你能夠會放一些和原始數據不相關的推斷出來的屬性或者工具方法,如Person類,你可能給他寫一個方法來根據原始數據年齡來判斷是否有資格作某事,好比結婚;可是MVVM中的M,根據個人理解,你直接用它來存放元數據(這裏,可能仍是有爭議的,僅是我的的理解與實踐).數組
第一個V,比MVC中的V要更普遍些,它包括 UIView 與 UIViewController及其子類,View用來顯示和交互,UIViewController擔當一種相似於橋樑的角色,來使 View 和 ViewModel部分更好通訊.網絡
餘下的"VM",實際上是一個總體,指的是ViewModel,視圖數據模型.若是你之前的許多代碼都放在Model中,好比沒有數據自動聯網請求相關的數據什麼的話,那你的那個Model其實和這個ViewModel有些像.MVVM中,要求Model更薄,最好只存儲原始數據信息;而對於其餘的設計到邏輯的代碼,建議都放到ViewModel中.你可能會說,這樣ViewModel 會不會很亂呢?未必!ViewModel中的代碼會不少,可是ViewModel的可複用性和靈活性要遠遠大於ViewController.更具體點說,之前的一個控制器裏面的代碼,如今可能會被拆分到1個甚至多個ViewModel中,並且你的ViewModel不只這個控制器能夠用,其餘的控制器也能夠用.雖然從單個控制器的邏輯代碼量來看,優化不是很顯著,可是ViewModel的模塊化特性,將在涉及到頁面複用以及後期維護時,讓人感受心曠神怡!
關於MVVM,網上還有一種觀點是,其實能夠不要Model層,直接使用ViewModel層來存儲數據.我的感受,若是考慮到單元測試,此時若是有單獨的Model部分,能夠根據一個Model,直接測試ViewModel的邏輯,是極好的,因此目前仍是繼續保留Model部分.另外,也是考慮到後期可能會設計到Model自己的變動,好比將Model由一個普通的NSObjet變爲CoreData的一個實體,能夠很容易地讓代碼支持本地化.
此時,我還在考慮的一點是,公司代碼其實Model部分不是由我負責的,若是想繼續引入MVVM改造項目,保留一個ViewModel層,也可使個人代碼對其餘項目成員的影響降到最低.想來也是極好的!
接下來,會以第一篇文章的示例爲基礎,將逐步改造爲MVVM模式.
個人觀點是,儘可能不要使用系統自帶的數據類型,好比數組,字典等做爲Model,要儘量地使用自定義地類.使用自定義的類,方便後期維護,也能夠避免一些基礎錯誤,如:自定義的類,若是屬性不匹配會編譯失敗,可是若是使用字典類型,key不匹配時,是不會有任何提示的(用過字典的童鞋,都懂我意思的吧).因此咱們此處要:
新增Model: YFCategoryArticleListModel,表示按分類分組的文章列表,其中有兩個字段:category,分類;articles,此分類下的文章列表.
新增ViewModel: YFBlogDetailViewModel 表示文章的視圖模型;YFBlogListViewModel 表示 分類文章列表的視圖模型; YFBlogListItemViewModel 表示文章列表單個單元格的視圖模型;
Model僅用於存儲數據,ViewModel的具體邏輯下面須要時,會具體分析.另外,必須提到一點的是 @青玉伏案,給我推薦了一個RAC的VM框架ReactiveViewModel,有興趣的能夠研究下.可是我不是很能理解這麼作的必要性,因此暫時我仍是按照我本身的理解,用最常規的方式來寫ViewModel部分.
就像我開篇序言中提到的那樣,MVVM系列的文章,不僅僅是關於MVVM的討論,更是關於如何將已有MVC項目逐步過渡爲MVVM架構的可行性以及方法步驟的探究.這裏我採用的是一種折中的更具可行性的方案: 我對外暴露的接口是ViewModel,可是對應的會給這個ViewModel提供一個使用Model做爲參數的便利初始化方法;控制器或模塊內部,就直接使用傳入的ViewModel.這樣,我以爲纔是極好的,一方面本身能夠踐行MVVM,提早踩踩坑,另外一方面也基本不會對其餘小夥伴的開發工做形成太多的困擾!具體到本文示例,具體指:
文章列表控制器: 爲了與MVC模式區分,新建控制器YFMVVMPostListViewController,並添加夠公有屬性viewModel,它是YFCategoryArticleListViewModel 類型.
文章詳情控制器: 爲了與MVC模式區分,新建控制器YF
MVVMPostListViewController,僅添加只讀屬性viewModel,它是YFArticleViewModel類型.
關於ViewModel的自定義下面會具體談到.
必須指出的一點是: ViewModel是爲View服務的,它的命名和字段定義應該根據View的須要來進行.本例是一個很是簡單的場景.在複雜的場景中,一個model可能對應多個viewModel,此時多個視圖可能都是同一種數據的不一樣展現方式;一個viewModel可能對應多個model,此時頁面比較複雜,設計到多種數據的展現.簡言之,應該是一個View對應一個ViewModel(這一點,可能也有待商榷,但暫時我會採起此種方式).因此,你的ViewModel中的屬性沒必要和某個Model有真正意義上的對應關係,而是應該根據它服務的View來寫和命名.
添加屬性intro: 這個viewModel 供展現博客列表中的單個單元格使用,但根據目前的UI顯示,只須要一個字段便可,咱們給它命名爲 intro,字符串屬性.這個後期能夠根據UI變化動態更改.就像上面提到的,ViewModel是爲Model服務的.
添加屬性 blogId.
添加初始化方法 -initWithArticleModel: 以便於從一個YFArticleModel對象構建視圖模型.
注意須要在初始化時設置 introl和model的title,desc屬性的級聯關係(我喜歡這麼稱呼,意會,有點重寫getter方法的感受).這一步原本是在Controller中完成的,如今挪到了 ViewModel中,Controller 不就瘦了一點了嗎,並且把這個邏輯寫到這裏還更方便代碼複用.
- (instancetype)initWithArticleModel:(YFArticleModel *)model { self = [super init]; if (nil != self) { // 設置intro屬性和model的屬性的級聯關係. RAC(self, intro) = [RACSignal combineLatest:@[RACObserve(model, title), RACObserve(model, desc)] reduce:^id(NSString * title, NSString * desc){ NSString * intro = [NSString stringWithFormat: @"標題:%@ 內容:%@", model.title, model.desc]; return intro; }]; // 設置self.blogId與model.id的相互關係. [RACObserve(model, id) subscribeNext:^(id x) { self.blogId = x; }]; } return self; }
添加屬性blogListItemViewModels,NSArray 類型,用於存儲文章列表單元格的視圖模型.視圖部分檢測它的變化,而後動態刷新視圖便可.
添加工具方法: -first 與 -next,用於支持常見的數據分頁操做,配合blogListItemViewModels,能夠實現常見的上拉刷新與加載加載的操做.
添加初始化方法 -initWithCategoryArtilceListModel, 用於快速使用一個分類文章列表數據模型來快速初始化.再次強調一次: model 和 viewModel 並非一一對應的關係,這裏只是爲了簡化從一種Model生成此種ViewModel的操做;即,之後若是有其餘種類的可使用此種ViewModel的話,咱們再爲其添加一個重新Model初始化的方法便可.
初始化時,涉及到網絡請求,在此處咱們額外引入了一個AFN擴展 AFNetworking-RACExtensions,用於使用RAC的語法格式使用AFN.
// 接口完整地址,確定是受分類和頁面的影響的.可是由於分類的變化最終會經過分頁的變化來體現,因此此處僅需監測分頁的變化狀況便可. [RACObserve(self, nextPageNumber) subscribeNext:^(NSNumber * nextPageNumber) { NSString * path = [NSString stringWithFormat: @"http://www.ios122.com/find_php/index.php?viewController=YFPostListViewController&model[category]=%@&model[page]=%@", self.category, nextPageNumber]; self.requestPath = path; }]; // 每次數據完整接口變化時,必然要同步更新 blogListItemViewModels 的值. [RACObserve(self, requestPath) subscribeNext:^(NSString * path) { /** * 分兩種狀況: 若是是變爲0,說明是重置數據;若是是大於0,說明是要加載更多數據;不處理向上翻頁的狀況. */ NSMutableArray * articls = [NSMutableArray arrayWithCapacity: 42]; if (YES != [self.nextPageNumber isEqualToNumber: @0]) { [articls addObjectsFromArray: self.blogListItemViewModels]; } [[self.httpClient rac_GET:path parameters:nil] subscribeNext:^(RACTuple *JSONAndHeaders) { // 使用MJExtension將JSON轉換爲對應的數據模型. NSArray * newArticles = [YFArticleModel objectArrayWithKeyValuesArray: JSONAndHeaders.first]; // RAC 風格的數組操做. RACSequence * newblogViewModels = [newArticles.rac_sequence map:^(YFArticleModel * model) { YFBlogListItemViewModel * vm = [[YFBlogListItemViewModel alloc] initWithArticleModel: model]; return vm; }]; [articls addObjectsFromArray: newblogViewModels.array]; self.blogListItemViewModels = articls; }]; }];
關於MVVM的優點,此處已可見一斑!咱們成功的從控制器中剝離了網絡請求以及數據分頁的相關代碼.從總體代碼量的角度,咱們可能沒少寫幾行代碼;可是從代碼複用性的角度考慮,咱們的代碼更具備可複用性,由於未來可能其餘地方也會用到這個頁面;與此同時,代碼之間的耦合性也下降了不少;可擴展性大大提升![PS: 關於代碼耦合性,可複用性什麼的,真的很大程度上是由模式自己決定的!]
添加屬性content,用於直接在網頁視圖上顯示,View內檢測這個屬性值,動態刷新視圖便可.
添加初始化方法 -initWithModel: 用於方便從一個 YFArticleModel 數據模型新建相應的視圖模型.
設計到網絡請求部分的核心代碼以下:
/** * 公共的與Model無關的初始化. */ - (void)setup { // 初始化網絡請求相關的信息. self.httpClient = [AFHTTPRequestOperationManager manager]; self.httpClient.requestSerializer = [AFJSONRequestSerializer serializer]; self.httpClient.responseSerializer = [AFJSONResponseSerializer serializer]; // 接口完整地址,確定是受id影響. [RACObserve(self, blogId) subscribeNext:^(NSString * blogId) { NSString * path = [NSString stringWithFormat: @"http://www.ios122.com/find_php/index.php?viewController=YFPostViewController&model[id]=%@", blogId]; self.requestPath = path; }]; // 每次完整的數據接口變化時,必然要同步更新 self.content 的值. [RACObserve(self, requestPath) subscribeNext:^(NSString * path) { [[self.httpClient rac_GET:path parameters:nil] subscribeNext:^(RACTuple *JSONAndHeaders) { // 使用MJExtension將JSON轉換爲對應的數據模型. YFArticleModel * model = [YFArticleModel objectWithKeyValues:JSONAndHeaders.first]; self.content = model.body; }]; }]; }
若是耐心比較下 -setup 方法中的代碼,會發現與上個VM的-setup有許多共同之處,這就啓發咱們,或許應該將網絡請求類從VM中進一步剝離出來,製做一個通用的網絡請求類.通用網絡請求類與單元測試的相關話題,會在下篇MVVM系列文章中專門講述,在此再也不繼續討論.
坦白說,RAC真的讓人很喜歡;可是,我在這裏想說的是, RAC 只是簡化編碼的工具而已--所謂工具,就是那種你掌握了能夠走的更快,不會也無傷大雅的東西!國內,部分文章過度渲染 RAC 與UIKit 的差別,甚至有人宣稱是另外一條徹底不一樣的學習曲線--真的很扯,邏輯上無異於就像宣稱沒有MFC,全部人都會餓死同樣!在此,就不過多吐槽了,反正我是很早就看過國內某些博主的關於RAC的文章,被博主忽悠忽悠的不行,最終得出的結論是,太難了,暫時不學!若是,你恰好看到這篇文章,我想對你說的是: 耐下心,花一兩天結合本身的工程和基礎的RAC語法,嘗試用RAC寫寫代碼試試,真的很贊,並且是有足夠的姿式徹底兼容之前的本身寫法的!View部分,在此我就暫時不用RAC中的寫法來替代block,代理等,儘量地在MVC的代碼上,適當修正,以證實兩者的某種程度上的協同做用.
控制器中的代碼,真的被精簡了很多,以博客列表控制器爲例,幾乎佔據1/2控制器代碼量的網絡請求與數據分頁的代碼,被簡化爲一句話:
[RACObserve(self.viewModel, blogListItemViewModels) subscribeNext:^(id x) { [self updateView]; }];
一樣的,博客詳情也精簡了很是多,忍不住想曬下完整代碼:
// // YFMVVMPostViewController.m // iOS122 // // Created by 顏風 on 15/10/21. // Copyright (c) 2015年 iOS122. All rights reserved. // #import "YFMVVMPostViewController.h" #import "YFBlogDetailViewModel.h" #import <ReactiveCocoa.h> @interface YFMVVMPostViewController () @property (strong, nonatomic) UIWebView * webView; @end @implementation YFMVVMPostViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. [RACObserve(self.viewModel, content) subscribeNext:^(id x) { [self updateView]; }]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } - (UIWebView *)webView { if (nil == _webView) { _webView = [[UIWebView alloc] init]; [self.view addSubview: _webView]; [_webView makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(UIEdgeInsetsMake(0, 0, 0, 0)); }]; } return _webView; } /** * 更新視圖. */ - (void) updateView { [self.webView loadHTMLString: self.viewModel.content baseURL:nil]; } @end
示例完整代碼: iOS122
下篇主題: MVVM系列的下一個主題,會對MVVM中網絡請求的邏輯進一步從VM剝離以及單元測試的一些技術性討論,歡迎回帖交流!