今天博主有一個MVVM與ReactiveCocoa的需求,遇到了一些困難點,在此和大家分享,希望能夠共同進步.
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版),沒有之一!
提到MVC,你現在可以先自己回想一下自己寫過的程序,然后再往下看.
所以說, UIKit自身就是為MVC模式設計的,而你就算不清除什么是MVC,但你的代碼其實就是MVC模式.當你閱讀自己以前的代碼或者別人的代碼時,經常感覺這個代碼寫的好亂(shi)啊,其實這真的不是自己或別人的鍋,這是MVC本身難以避免甚至必然會出現的一個坑!所以,后來有人借鑒其他語言,提出了MVVM模式,并躬身實踐!
首先,MVVM,從概念說上來說,真的很好,很吸引人,即使你可能看不太懂,也感覺很高大上的樣子!但是,當你真的去百度相關概念時,往往會很納悶,似乎比我現在還麻煩,甚至開始懷疑,MVVM應該還只停留在理論階段吧!
--NO,只是因為你沒有找到合適的文章,沒有找到合適的工具--ReactiveCocoa!還是先說一下 MVVM的基礎概念吧,不然沒法往下說了:
關于MVVM,網上還有一種觀點是,其實可以不要Model層,直接使用ViewModel層來存儲數據.
個人感覺,如果考慮到單元測試,此時如果有單獨的Model部分,可以根據一個Model,直接測試ViewModel的邏輯,是極好的,所以目前還是繼續保留Model部分.
另外,也是考慮到后期可能會設計到Model本身的變更,比如將Model由一個普通的NSObjet變為CoreData的一個實體,可以很容易地讓代碼支持本地化.
此時,我還在考慮的一點是,公司代碼其實Model部分不是由我負責的,如果想繼續引入MVVM改造項目,保留一個ViewModel層,也可以使我的代碼對其他項目成員的影響降到最低.想來也是極好的!
接下來,會以第一篇文章的示例為基礎,將逐步改造為MVVM模式.
我的觀點是,盡量不要使用系統自帶的數據類型,比如數組,字典等作為Model,要盡可能地使用自定義地類.
使用自定義的類,方便后期維護,也可以避免一些基礎錯誤,如:自定義的類,如果屬性不匹配會編譯失敗,但是如果使用字典類型,key不匹配時,是不會有任何提示的(用過字典的童鞋,都懂我意思的吧).所以我們此處要:
Model僅用于存儲數據,ViewModel的具體邏輯下面需要時,會具體分析.另外,必須提到一點的是@青玉伏案,給我推薦了一個RAC的VM框架 ReactiveViewModel ,有興趣的可以研究下.但是我不是很能理解這么做的必要性,所以暫時我還是按照我自己的理解,用最常規的方式來寫ViewModel部分.
就像我開篇序言中提到的那樣,MVVM系列的文章,不單單是關于MVVM的討論,更是關于如何將已有MVC項目逐步過渡為MVVM架構的可行性以及方法步驟的探究.
這里我采用的是一種折中的更具可行性的方案: 我對外暴露的接口是ViewModel,但是對應的會給這個ViewModel提供一個使用Model作為參數的便利初始化方法;控制器或模塊內部,就直接使用傳入的ViewModel.
這樣,我覺得才是極好的,一方面自己可以踐行MVVM,提前踩踩坑,另一方面也基本不會對其他小伙伴的開發工作造成太多的困擾!具體到本文示例,具體指:
關于ViewModel的自定義下面會具體談到.
必須指出的一點是: ViewModel是為View服務的,它的命名和字段定義應該根據View的需要來進行.本例是一個非常簡單的場景.在復雜的場景中,一個model可能對應多個viewModel,此時多個視圖可能都是同一種數據的不同展示方式;一個viewModel可能對應多個model,此時頁面比較復雜,設計到多種數據的展示.簡言之,應該是一個View對應一個ViewModel(這一點,可能也有待商榷,但暫時我會采取此種方式).所以,你的ViewModel中的屬性不必和某個Model有真正意義上的對應關系,而是應該根據它服務的View來寫和命名.
- (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;}
// 接口完整地址,肯定是受分類和頁面的影響的.但是因為分類的變化最終會通過分頁的變化來體現,所以此處僅需監測分頁的變化情況即可.[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: 關于代碼耦合性,可復用性什么的,真的很大程度上是由模式本身決定的!]
/** * 公共的與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
http://mp.weixin.QQ.com/s?plg_nld=1&plg_uin=1&mid=400435726&idx=1&plg_nld=1&scene=23&plg_auth=1&__biz=MjM5MDE0Mjc4MA%3D%3D&plg_dev=1&srcid=11032ZYiaJix9NRGyZo1sPbj&plg_usr=1&plg_vkey=1&sn=8bcca03cd94f053dccd69b1a700cac3d#wechat_redirect&appinstall=0
新聞熱點
疑難解答