一次MVVM+ReactiveCocoa實踐,mvvmreactivecocoa

來源:互聯網
上載者:User

一次MVVM+ReactiveCocoa實踐,mvvmreactivecocoa
前言

學習MVVM和ReactiveCocoa(簡稱RAC)也有一段時間了,不過都僅限於看部落格,一直對這兩個東西高度興趣,覺得很創新,也一直想找個機會在項目中實踐一下,但是還是有一些顧慮,畢竟沒有實踐過,網上的資料看的也有點雲裡霧裡,實際上手可能還是有一定的難度。於是決定寫一個簡單的demo實踐一下。我特意選擇了一個剛剛寫的項目中的一個介面來實現,為的是能從實際項目需求出發,看看換成MVVM+RAC該如何?。(關於MVVM和ReactiveCocoa的基礎介紹我這裡就不在說了,網上有相關資料可以查閱)

所實現的功能

所實現的功能很簡單,就一個列表介面,UITableView搞定,可以下拉重新整理,上拉載入更多。最終的效果如下:

所採用的項目結構

Model:實體
View:Storyboard、xib和自訂view
ViewController:就是UIViewController了,我們要實現的介面對應的Controller就是ProductListViewController
ViewModel:(這個怎麼翻譯呢?視圖實體?)你們懂的。
API:網路請求相關

用到的第三方庫:

1 pod 'AFNetworking', '~> 2.5.3'2 pod 'ReactiveCocoa', '~> 2.5'3 pod 'MJRefresh', '~> 2.4.7'4 pod 'MJExtension', '~> 2.5.9'5 pod 'AFNetworking-RACExtensions', '~> 0.1.8'

除了AFNetworking和ReactiveCocoa,就是MJ大神的2個很受歡迎的類庫了,都是很常用的吧。(此處容我做個悲傷的表情,我開始寫這個demo的時候RAC3.0版本還只是alpha、beta版本,所以我用了2.0最終的一個正式版2.5,但是在寫這篇文章的時候,我又pod search了一下,發現已經出到4.0alpha版本了,不知道4.0又有了哪些改動,但是我知道3.0版本裡RACCommand被標記成了deprecate,由RACAction替代,用法應該差不多)

實現細節(MVVM與ReactiveCocoa結合)

 

擷取列表資料

 

我們都知道在MVVM裡,跟網路通訊相關的操作都是應該由ViewModel來處理的,所以在ProductListViewModel裡定義了一個RACCommand,我們叫:

1 /**2  *  擷取資料Command3  */4 @property (nonatomic, strong, readonly) RACCommand *fetchProductCommand;

在ViewModel的init方法裡對它進行初始化:

1 _fetchProductCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {2         3         return [[[APIClient sharedClient]4                  fetchProductWithPageIndex:@(1)]5                  takeUntil:self.cancelCommand.executionSignals];6     }];

訂閱RACCommand,擷取資料後賦值給items(items是儲存所有資料的數組,即tableView的dataSource)

 1    @weakify(self); 2     [[_fetchProductCommand.executionSignals switchToLatest] subscribeNext:^(ResponseData *response) { 3         @strongify(self); 4         if (!response.success) { 5             [self.errors sendNext:response.error]; 6         } 7         else { 8             self.items = [ProductListModel objectArrayWithKeyValuesArray:response.data]; 9             self.page = response.page;10         }11     }];

再看ProductListViewController裡,訂閱ViewModel的items,有變化時就reload tableview。

1     [RACObserve(self.viewModel, items) subscribeNext:^(id x) {2         @strongify(self);3         [self.table reloadData];4     }];

tableView的dataSource如下:

 1 #pragma mark - UITableViewDataSource 2  3 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 4     return 1; 5 } 6  7 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 8     return self.viewModel.items.count; 9 }10 11 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {12     ProductListCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ProductListCell" forIndexPath:indexPath];13     cell.viewModel = [self.viewModel itemViewModelForIndex:indexPath.row];14     15     return cell;16 }

再看自訂tableViewCell裡:

 1 - (id)initWithCoder:(NSCoder *)aDecoder { 2     self = [super initWithCoder:aDecoder]; 3      4     if (self) { 5         @weakify(self); 6         [RACObserve(self, viewModel) subscribeNext:^(id x) { 7              8             @strongify(self); 9             self.productNameLabel.text = self.viewModel.ProductName;10             self.bankNameLabel.text = self.viewModel.ProductBank;11             self.profitLabel.text = self.viewModel.ProductProfit;12             self.saleStatusLabel.text = self.viewModel.SaleStatusCn;13             self.productTermLabel.text = self.viewModel.ProductTerm;14             self.productAmtLabel.text = self.viewModel.ProductAmt;15             16         }];17     }18     19     return self;20 }

有RAC就是這麼方便,不要block回調,更無須delegate。

擷取更多資料

上拉載入更多,MJ已經幫我們處理了。我們只需要在ViewModel裡定義一個載入更多資料的RACCommand供調用即可。這裡就不介紹了,具體可以看最終的demo。

UITableView 重新整理狀態切換

用過MJRefresh的都知道,不管是header還是footer,beginRefreshing後,擷取完資料後是需要調用endRefreshing來切換重新整理狀態的。用RAC來實現的話,我們可以訂閱RACCommand的executing訊號,如下:

1     @weakify(self)2     [_viewModel.fetchProductCommand.executing subscribeNext:^(NSNumber *executing) {3         NSLog(@"command executing:%@", executing);4         if (!executing.boolValue) {5             @strongify(self)6             [self.table.header endRefreshing];7         }8     }];

上面差不多就是ViewModel和ViewController之前的邏輯互動,他們之間就是通過ReactiveCocoa這座橋來串連的。

關於http請求這塊,AFNetworking大家都比較熟悉用法了,AFNetworking-RACExtensions就是把AFNetworking裡的http請求轉成了RACSignal,在ReactiveCocoa的世界裡,一切都是Signal(不知道說的對不對╮(╯_╰)╭)。

我封裝了一個httpGet方法:

 1 - (RACSignal *)httpGet:(NSString *)URLString parameters:(id)parameters { 2     return [[[self rac_GET:URLString parameters:parameters] 3             catch:^RACSignal *(NSError *error) { 4                 //對Error進行處理 5                 NSLog(@"error:%@", error); 6                 //TODO: 這裡可以根據error.code來判斷下屬於哪種網路異常,分別給出不同的錯誤提示 7                 return [RACSignal error:[NSError errorWithDomain:@"ERROR" code:error.code userInfo:@{@"Success":@NO, @"Message":@"Bad Network!"}]]; 8             }] 9             reduceEach:^id(id responseObject, NSURLResponse *response){10                 NSLog(@"url:%@,resp:%@",response.URL.absoluteString,responseObject);11                 ResponseData *data = [ResponseData objectWithKeyValues:responseObject];12                 13                 return data;14             }];15 }

裡面主要幹了兩件事,第一是錯誤處理(下面會講到),第二是對返回資料進行解析,一般都是把json資料轉成Model。

在實際項目中,基本上所有api介面的傳回值格式都是統一的(不統一的話你可以去打服務端的人了),所以我定義了一個叫ResponseData的Model,這個Model裡有個NSObject類型的屬性,用來接收不同類型的值(數組、對象(即字典)等)。這樣的話每個api介面根據實際情況對這個NSObject類型的屬性進行格式轉換即可,使用起來就很方便了。

錯誤處理

錯誤處理又可以分好幾種情況,比如:
1)網路錯誤(無網路,逾時等)
2)伺服器端錯誤(404、500等)
3)商務邏輯錯誤
前兩種錯誤,都會進入RACCommand的errors訊號通道,在上面封裝的那個httpGet方法裡可以看到,我們catch了error,然後就可以根據error的code來區分是哪種錯誤,這麼區分的目的是給使用者展示不同的錯誤提示,更加友好。
而第三種“錯誤”其實服務端返回的也是一個正常的json字串,我們也是會將它解析成ResponseData對象,這個時候就得單獨判斷是否出現錯誤了。針對兩種不同的情況,如果要分開處理,那必然會有很多重複的代碼,作為一個追求高品質代碼的程式猿來說,這是不可取的方案(甚至是不能忍的)。我的處理方案是(參考了http://limboy.me/ios/2014/06/06/deep-into-reactivecocoa2.html中關於RACSubject的用法):

1)定義一個BaseViewModel作為所有ViewModel的基類

 1 @interface BaseViewModel : NSObject 2  3 @property (nonatomic) RACSubject *errors; 4  5 /** 6  *  取消請求Command 7  */ 8 @property (nonatomic, strong, readonly) RACCommand *cancelCommand; 9 10 @end

2)對RACCommand的errors進行合并:

1 [[RACSignal merge:@[_fetchProductCommand.errors, self.fetchMoreProductCommand.errors]] subscribe:self.errors];

3)在RACCommand的訂閱裡判斷是否出現error,如果有錯誤,手動send一個error。

 1   @weakify(self); 2     [[_fetchProductCommand.executionSignals switchToLatest] subscribeNext:^(ResponseData *response) { 3         @strongify(self); 4         if (!response.success) { 5             [self.errors sendNext:response.error]; 6         } 7         else { 8             self.items = [ProductListModel objectArrayWithKeyValuesArray:response.data]; 9             self.page = response.page;10         }11     }];

4)ViewController裡對ViewModel裡的errors進行訂閱。

1 [_viewModel.errors subscribeNext:^(NSError *error) {2         ResponseData *data = [ResponseData objectWithKeyValues:error.userInfo];3         NSLog(@"something error:%@", data.keyValues);4         //TODO: 這裡可以選擇一種合適的方式將錯誤資訊展示出來5     }];

原則就是把所有的錯誤都統一到一個通道裡,這樣只需要在一個地方處理就行了。

http請求cancel

我們在實現某些介面功能時,往往會在介面開啟後進行http請求,有時會顯示一個指標告訴使用者正在請求資料。但是如果網路比較差的情況下(比如2G網),有時使用者可能覺得等的時間太長了,就點了返回,介面雖然是關閉了,但是對於那個http請求來說它還在繼續的。這個時候比較好的處理方式就是將那個http請求cancel掉。不用RAC的情況下,我們需要記錄每次發起http請求的NSURLSessionTask(如果你是用的AFNetworking的AFHTTPSessionManager的話),然後在Viewcontroller的dealloc裡調用【task cancel】來取消這個task,需要注意的時,task被cancel的時候會返回error,這個時候就需要判斷下errorCode來甄別是不是cancel,以免跟其他網路異常弄混。
那麼用ReactiveCocoa該怎麼實現http的cancel呢?好在AFNetworking-RACExtensions’已經幫我們封裝好了,我們只需要在ViewModel裡定義一個表示取消http請求的RACCommand(可以放到BaseViewModel裡),然後再必要的地方調用這個command即可,當然前提是我們在發起http請求的command裡設定了如下的代碼:

1 _fetchProductCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {2         3         return [[[APIClient sharedClient]4                  fetchProductWithPageIndex:@(1)]5                  takeUntil:self.cancelCommand.executionSignals];6     }];

核心點就在於takeUntil,它表示“一直執行直到…”,套用在我們這裡就是http請求一直執行,直到cancel命令被下達。經過測試可以發現完全能達到我們的目的。
PS:這裡額外介紹下如何類比不穩定的網路。設定 -> 開發人員 -> NETWORK LINK CONDITIONER,裡面有各種選項可供選擇,比如100% Loss,3G,Very Bad Network等,雖然沒有專業工具那麼強大,但是簡單類比下異常網路也是足夠了。

Model與ViewModel的界定

這兩者關係說清晰也清晰,說不清晰也不清晰。

為什麼說清晰呢?因為Model是實體,一般就是一些屬性欄位而已,而ViewModel是介於ViewController於Model之間的橋樑,ViewModel裡有RACCommand,也會有一些商務邏輯(比如分頁處理,ViewController只需要調用fetchData或者fetchMoreData即可,無需知道現在顯示的是第幾頁)。

那為什麼又不清晰呢?在我這個demo裡有個自訂tablecell的ViewModel(ProductListCellViewModel),這裡面其實也就是一些屬性而已,跟ProductListModel基本上都是一樣的。所以遇到這種情況就比較迷惑,到底是拿Model當ViewModel用呢,還是分開冗餘一部分代碼呢?而且http請求返回的資料一般就是ViewController需要顯示的資料(只是一般情況,也有需要額外處理的)。

到底該怎麼處理呢?說說我的理解:
1)從http請求獲得的資料,就是sourceData,而我們的Model就是作為sourceData而存在的,所以我更傾向於用Model來映射json資料。
2)ViewModel是拿到Model進行處理(有時可能不需要額外處理),然後提供給ViewController使用,比如直接顯示到View上。

這也真是MVVM架構的核心。所以ViewModel裡的items儲存的是Model的數組。那麼問題又來了,既然items裡是Model,而ViewController又是通過ViewModel擷取sourceData,那從Model到ViewModel該在哪裡進行轉換呢?

我能想到的是3個方案:
1)使用Model解析json資料後,迴圈遍曆Model轉成ViewModel儲存到items裡。這種做法,items裡儲存的是ViewModel而不是Model,TableCell使用的時候直接拿items裡的ViewModel即可。
2)items儲存Model,TableCell直接使用Model。當Model跟ViewModel幾乎完全一致的情況下很有可能會出現這種情況。因為會覺得完全複製一個ViewModel出來不值,但是這又不太符合MVVM。
3)items儲存Model,TableCell擷取ViewModel時,通過Model初始化ViewModel。
我目前使用的是第3種方案,在ViewModel裡使用Model作為一個屬性,然後提供一些readonly的屬性並重寫其get方法(中間可以對資料進行一些格式化之類的)供介面使用。

遇到的坑

獨自學習RAC還是有一定的難度的,畢竟面對眾多RAC的api要想完全理解下來還是挺困難的。而且剛開始不熟悉的情況下很難針對某些特定的情境,想出比較合理的RAC處理方式(這句話是盜用別人的,但是我也深有體會)。

這裡列一下我寫這個demo時遇到的幾個坑吧,希望能幫別人繞過這些坑,也算是功德一件。
1)ViewModel裡用來儲存資料的數組,不能使用NSMutableArray。原因是RAC是基於KVO的,而NSMutableArray的Add和Remove方法並不會給KVO發送通知,因此對NSMutableArray進行RACObserve時,並不會達到我們想要的結果。(同理其他Mutable的也都不能用)
2)ViewModel裡給items賦值時,不能用_items=somearray,而是得用self.items。我開始是想在viewmodel裡定義一個readonly的items屬性(理論上也應該是readonly的,因為ViewController只負責從ViewModel拿資料而已),然後通過_items進行賦值,但是訂閱了viewmodel的items後死活收不到訊息。我一直感覺這不科學,也許是我的開啟檔案不對,但是最終都沒有解決。這裡希望知道的人能不吝賜教,在下感激不盡。
3)實現可以cancel的http請求時,不能用replay,replayLast,replayLazily。關於這3者的區分可以參考這個,我覺得分析的很詳細。

總結

以上就是我的一次MVVM+RAC的實踐,初學MVVM和RAC,難免有些概念和理解有偏差,歡迎批評指正,也歡迎一起交流討論。為的是能更好的學習和進步!

這裡奉上我的demo源碼:傳送門

(因為demo所用介面是實際項目介面,容我將其抹掉)

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.