此文是翻譯作品,原文見:http://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-1
此文是我學習過程中遇到的很好的文章,因為搜不到翻譯版本,因此自己翻譯了,希望能幫到大家。同時翻譯的時候我也好好精進了一下我的markdown語法
你可能在Twitter上聽過這樣的笑話:
“iOS框架,大量View Controller的產生地” by Colin Campbell。
這在iOS開發者心中是個輕松地“戳”,但是我確信你已經在練習中遇到過這些問題了——臃腫的,難以管理的View Controller。
這個MVVM的開發教程用一個不同的模式來構建一個app,Model-View-View-ViewModel,或者簡稱MVVM,這個模式因為ReactiveCocoa的誕生更加方便,帶來了一個完美的MVC模式的替換模式,和一個輕便的,易于管理的View Controller!
通過這個MVVM教程,你要去建立一個簡單的搜索app叫做Flicker search,像下面的圖片一樣:
注意:這個教程是使用Objective-C開發的,如果你要看我用swift開發的教程,點擊這里,在我的博客里面可以看到。
在你開始寫代碼前,是時候講一些理論知識了!
一個對ReactiveCocoa的簡單介紹
這個教程主要是關于MVVM的,并且假設你對ReactiveCocoa有一定的了解,如果你沒有用過ReactiveCocoa,我強烈建議你看我早一些的教程,這個教程會教給你很多。
ReactiveCocoa最核心的東西無疑是 signals,在RACSignal 這個類里面。signals給事件發出一個流,這個流(stream)有三種類型: next、 completed 和 error。
運用這些簡單的模式,ReactiveCocoa 可以用來替代代理模式(delegate pattern),觀察者模式(KVO)和 target-action pattern,以及更多。
用signal的API編寫出來的代碼更加均勻,因此更加容易閱讀,但是ReactiveCocoa真正的強大的地方在于是你對signals的高級操作,這些操作允許你進行復雜的過濾(filter),轉化(transformation)以及用簡單的方式協調(coordination)。
在MVVM的環境下,ReactiveCocoa扮演了極其重要的角色,它提供了強大的粘合力在View和ViewModel中間,這些對你還有一點點的超前。
MVVM開發模式的介紹
MVVM——Model-View-ViewModel,在通常的理解中是一個設計的模式,他是MV家族的一個成員,這個家族包括MVC、MVP等等。
每一個MV家族中的模式開始關心如何將UI和業務邏輯分開,因為這樣更便利于開發和測試。
注意:如果想要深入了解開發設計模式,我推薦Eli’s和Ash Furrow’s的文章。
了解MVVM的起源有助于你更加了解這個模式。
MVC是第一個用戶界面設計模式( UI design pattern),可以追溯到1970年代的Smalltalk language。下面這個圖說明了MVC的主要運作模式:
這個模式將用戶界面分為三種:
- Model,用來呈現應用狀態。
- View,由視圖控制器組成。
- Controller,處理用戶交互并且更新model。
MVC的一個重大問題令人十分困擾,這個概念很好很完美,但是當經常人們開始實現MVC的時候,Model-View-Controller看似圓形的關系,反過來,他們合并成了一個可怕的巨大的麻煩。
不久之前Martin Fowler 向我們介紹了一個由MVC衍生出來的表現模式,并被微軟接受并流行開來。
這個模式的核心是ViewModel,是一種特殊的Model,用來展示應用中UI的狀態。
它包含了每一個UI控制器(Controller)的詳細狀態和屬性,例如,一個TextFeild當前的文字,或者一個按鈕的可否點擊的狀態,它也展現了當前視圖的一系列動作,例如按鈕點擊或者手勢操作。
將VIewModel理解成為View的Model(model of the view)可以更好地幫你去思考ViewModel。
MVVM遵循以下規則
- 1.View用來展現VIewModel,但是VIewModel不能展現View。
- 2.VIewModel用來展現Model,但是也不可反過來。
如果你打破了任何這個規則,你的MVVM就錯了!
這種規則的優勢如下:
- 1.更加輕量級的VIew層,所有業務邏輯都被移到ViewModel中。
- 2.更易于測試,你可以在沒有View的情況下啟動你的應用,大大提高了可測試性。
注意:測試視圖是眾所周知的困難,因為測試運行的小的包含的代碼塊。通常,控制器會在依賴于其他應用程序狀態的場景中添加和配置視圖。這意味著,意義上的小測試,可以成為一個脆弱而繁瑣的命題。
因此,你可能會想提出一個問題,如果只是View可以展現VIewModel,而ViewModel不能反過來展現View的話,那么ViewModel如何更新View呢?啊哈!!這就是MVVM的秘訣了!
MVVM和數據綁定(Data Binding)
MVVM模式依賴于數據綁定,一個框架級的功能,自動連接對象屬性的用戶界面控件。
有一個例子,在微軟的WPF框架,下面一個例子將TextField的文本和ViewModel的Username綁定。
<TextField Text=”{DataBinding Path=Username,Mode=TwoWay}”/>
WPF的框架將這兩個成員變量“綁定”。
這個雙向的綁定確保了ViewModel的Username的改變同時TextFeild的文本也改變,反之亦然,用戶的輸入也將改變ViewModel中的參數值。
另一個例子,基于web的流行的一個MVVM框架Knockout, 你可以發現兩個框架中數據綁定的相同的特點。
<input data-bind=”value: username”/>
上面的綁定將HTML的元素和JavaScript的模型綁定。
不幸的是,iOS缺少一個數據綁定框架,但是這就是ReactiveCocoa所充當的“膠水”作用。
具體從iOS開發的角度去看MVVM,ViewController和它相關的UI——不論是xib、storyboard或者是代碼組成的視圖(View):
ReactiveCocoa將兩者綁定起來。
注意:對于UI的各種實現方式,我高度推薦Martin Fowler的GUI Architectures article。
你學到了足夠的理論知識了嗎?如果沒有,請回頭去再看一遍。當然,如果你學得夠好了,那么現在是時候開始創造你自己的ViewModel了。
開始項目準備
首先下載這個開始工程
這個項目使用CocoaPods去管理依賴庫(如果你不知道CocoaPods,我們這里有個教程),運行pod install去安裝依賴庫,確認你看到了一下輸出:
$ pod install
Analyzing dependencies
Downloading dependencies
Installing LinqToObjectiveC (2.0.0)
Installing ReactiveCocoa (2.1.8)
Installing SDWebImage (3.6)
Installing objectiveflickr (2.0.4)
Generating Pods project
Integrating client project
你會學到每個依賴庫是干嗎的。
本教程的開始工程包含了一個View,通過xib實現,打開RWTFlickrSearch.xcworkspace,并且運行,然后你會看到以下頁面:
花一點時間去熟悉這個項目結構:
Model和VIewModel的groups都是空的,你要為這兩個group添加文件,項目已含有的文件是做這些的:
- RWTFlickSearchViewController:項目的主頁面,包含了一個搜索框,和一個“GO”按鈕。
- RWTRecentSearchItemTableViewCell:一個cell顯示來自Flicker的第三方圖片
是時候開始寫你的第一個view model 了!
你的第一個ViewModel
在ViewModel這個group里添加一個新的類,將之命名為RWTFlickrSearchViewModel并且使他繼承NSObject。
打開它并在頭文件添加下面的聲明:
@interface RWTFlickrSearchViewModel : NSObject
@property (strong, nonatomic) NSString *searchText;
@property (strong, nonatomic) NSString*title;
@end
searchText提供一個字符串顯示在textfield上,成員變量title提供在navigation bar上顯示的標題。
注意 :為了更容易的理解項目結構,View和ViewModel用了相同的名字和不同的后綴,例如:RWTFlickrSearch-ViewModel
和 RWTFlickrSearch-ViewController。
打開 RWTFlickrSearchViewModel.m 并且添加如下代碼
@implementation RWTFlickrSearchViewModel
- (instancetype)init {
self = [super init];
if (self) {
[self initialize];
}
return self;
}
- (void)initialize {
self.searchText = @"search text";
self.title = @"Flickr Search";
}
@end
這段代碼初始化了這個ViewModel。
下一步是講如何將ViewModel和View關聯到一起,記住View和ViewModel的關聯,因此就需要在View中給對應的ViewModel添加一個相關的實例化方法。
注意:在這個教程管我們的Controller叫做”Views“,這筆“View”在MVVM更多語義。和UIKit使用的默認名不同。
打開 RWTFlickrSearchViewController.h 并聲明ViewModel的頭文件。
#import "RWTFlickrSearchViewModel.h"
然后加入下面的實例化方法
@interface RWTFlickrSearchViewController : UIViewController
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel;
@end
在RWTFlickrSearchViewController.m中添加一個私有變量
@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel;
接下來實現init方法
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel {
self = [super init];
if (self ) {
_viewModel = viewModel;
}
return self;
}
注意:這是個弱引用(弱指針),View引用了ViewModel,但沒有擁有它。
在 viewDidLoad的最后加上下面代碼
[self bindViewModel];
下面是這個方法的實現
- (void)bindViewModel {
self.title = self.viewModel.title;
self.searchTextField.text = self.viewModel.searchText;
}
上面的代碼將會在UI初始化和ViewModel狀態在VIew上應用的時候運行。
最后一步是實例化ViewModel,并在View中應用。
在viewDidLoad中添加以下
#import "RWTFlickrSearchViewModel.h"
加一個私有變量(在文件頂部的類擴展名內)。
@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel;
你會發現已經有了一個createInitialViewController方法,更新他的實現方法:
- (UIViewController *)createInitialViewController {
self.viewModel = [RWTFlickrSearchViewModel new];
return [[RWTFlickrSearchViewController alloc]initWithViewModel:self.viewModel];
}
這將創建一個新的ViewModel實例,然后它返回View。這是應用程序的導航控制器的初始視圖。
恭喜你,這是你的第一個ViewModel。我得請你控制住你的興奮!這里還有很多要學習。
你可能已經注意到了你沒有使用任何ReactiveCocoa呢。在其目前的形式,任何用戶進入搜索文本字段將不會反映在ViewModel。
檢測有效搜索狀態
在這一部分中,你將使用ReactiveCocoa綁定ViewModel和View的搜索框和按鈕在一起,更新RWTFlickrSearchViewController.m中的bindViewModel方法如下
- (void)bindViewModel {
self.title = self.viewModel.title;
RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
}
我們來加一個ReactiveCocoa 中UITextFeild的分類的方法rac_textSignal,它是一個信號,每次文本字段更新時,將發出包含當前文本的一個事件,RAC宏是一個綁定,上面代碼更新了ViewModel中的searchText對象的值,它隨著rac_textSignal響應。
總之,上面的代碼保證了searchText的值總是UI中的最新的值。如果上面的寫法讓你感到陌生,你真的應該重新學習一下ReactiveCocoa tutorials這個教程!
如果用戶輸入的文本是有效的,則只能啟用搜索按鈕。這里的輸入規則是,他們必須輸入超過三個字符,然后才能執行搜索。
在 RWTFlickrSearchViewModel.m加入下面代碼
#import <ReactiveCocoa/ReactiveCocoa.h>
更新方法initialize:
- (void)initialize {
self.title = @"Flickr Search";
RACSignal *validSearchSignal =
[[RACObserve(self, searchText)
map:^id(NSString *text) {
return @(text.length > 3);
}]
distinctUntilChanged];
[validSearchSignal subscribeNext:^(id x) {
NSLog(@"search text is valid %@", x);
}];
}
編譯,運行并在TextFeild輸入一些文字。每次文本在有效或無效狀態之間轉換時,都會看到日志消息:
2014-05-27 18:03:26.299 RWTFlickrSearch[13392:70b] search text is valid 0
2014-05-27 18:03:28.379 RWTFlickrSearch[13392:70b] search text is valid 1
2014-05-27 18:03:29.811 RWTFlickrSearch[13392:70b] search text is valid 0
上面的代碼使用 RACObserve宏創建了一個ViewModel中的 searchText的信號。map操作將文本流轉換為真值和假值。最后, distinctuntilchanges是用來確保該信號只在狀態變化的時候傳遞值。