此文是翻譯作品,原文見:http://www.raywenderlich.com/74106/mvvm-tutorial-with-reactivecocoa-part-1
此文是我學習過程中遇到的很好的文章槽华,因為搜不到翻譯版本嚷缭,因此自己翻譯了赏酥,希望能幫到大家碑定。同時翻譯的時候我也好好精進了一下我的markdown語法
你可能在Twitter上聽過這樣的笑話:
“iOS框架奕纫,大量View Controller的產(chǎn)生地” by Colin Campbell涩馆。
這在iOS開發(fā)者心中是個輕松地“戳”行施,但是我確信你已經(jīng)在練習中遇到過這些問題了——臃腫的,難以管理的View Controller魂那。
這個MVVM的開發(fā)教程用一個不同的模式來構建一個app蛾号,Model-View-View-ViewModel,或者簡稱MVVM涯雅,這個模式因為ReactiveCocoa的誕生更加方便鲜结,帶來了一個完美的MVC模式的替換模式,和一個輕便的活逆,易于管理的View Controller精刷!
通過這個MVVM教程,你要去建立一個簡單的搜索app叫做Flicker search蔗候,像下面的圖片一樣:
注意:這個教程是使用Objective-C開發(fā)的贬养,如果你要看我用swift開發(fā)的教程,點擊這里,在我的博客里面可以看到琴庵。
在你開始寫代碼前误算,是時候講一些理論知識了!
一個對ReactiveCocoa的簡單介紹
這個教程主要是關于MVVM的迷殿,并且假設你對ReactiveCocoa有一定的了解儿礼,如果你沒有用過ReactiveCocoa,我強烈建議你看我早一些的教程,這個教程會教給你很多。
ReactiveCocoa最核心的東西無疑是 signals庆寺,在RACSignal 這個類里面蚊夫。signals給事件發(fā)出一個流,這個流(stream)有三種類型: next懦尝、 completed 和 error知纷。
運用這些簡單的模式,ReactiveCocoa 可以用來替代代理模式(delegate pattern)陵霉,觀察者模式(KVO)和 target-action pattern琅轧,以及更多。
用signal的API編寫出來的代碼更加均勻踊挠,因此更加容易閱讀乍桂,但是ReactiveCocoa真正的強大的地方在于是你對signals的高級操作,這些操作允許你進行復雜的過濾(filter),轉化(transformation)以及用簡單的方式協(xié)調(coordination)睹酌。
在MVVM的環(huán)境下权谁,ReactiveCocoa扮演了極其重要的角色,它提供了強大的粘合力在View和ViewModel中間憋沿,這些對你還有一點點的超前旺芽。
MVVM開發(fā)模式的介紹
MVVM——Model-View-ViewModel,在通常的理解中是一個設計的模式辐啄,他是MV家族的一個成員采章,這個家族包括MVC、MVP等等则披。
每一個MV家族中的模式開始關心如何將UI和業(yè)務邏輯分開共缕,因為這樣更便利于開發(fā)和測試洗出。
注意:如果想要深入了解開發(fā)設計模式士复,我推薦Eli’s和Ash Furrow’s的文章。
了解MVVM的起源有助于你更加了解這個模式翩活。
MVC是第一個用戶界面設計模式( UI design pattern)阱洪,可以追溯到1970年代的Smalltalk language。下面這個圖說明了MVC的主要運作模式:
這個模式將用戶界面分為三種:
- Model菠镇,用來呈現(xiàn)應用狀態(tài)冗荸。
- View,由視圖控制器組成利耍。
- Controller蚌本,處理用戶交互并且更新model。
MVC的一個重大問題令人十分困擾隘梨,這個概念很好很完美程癌,但是當經(jīng)常人們開始實現(xiàn)MVC的時候,Model-View-Controller看似圓形的關系轴猎,反過來嵌莉,他們合并成了一個可怕的巨大的麻煩。
不久之前Martin Fowler 向我們介紹了一個由MVC衍生出來的表現(xiàn)模式捻脖,并被微軟接受并流行開來锐峭。
這個模式的核心是ViewModel,是一種特殊的Model可婶,用來展示應用中UI的狀態(tài)沿癞。
它包含了每一個UI控制器(Controller)的詳細狀態(tài)和屬性,例如矛渴,一個TextFeild當前的文字抛寝,或者一個按鈕的可否點擊的狀態(tài),它也展現(xiàn)了當前視圖的一系列動作,例如按鈕點擊或者手勢操作盗舰。
將VIewModel理解成為View的Model(model of the view)可以更好地幫你去思考ViewModel晶府。
MVVM遵循以下規(guī)則
- 1.View用來展現(xiàn)VIewModel,但是VIewModel不能展現(xiàn)View钻趋。
- 2.VIewModel用來展現(xiàn)Model川陆,但是也不可反過來。
如果你打破了任何這個規(guī)則蛮位,你的MVVM就錯了较沪!
這種規(guī)則的優(yōu)勢如下:
- 1.更加輕量級的VIew層,所有業(yè)務邏輯都被移到ViewModel中失仁。
- 2.更易于測試尸曼,你可以在沒有View的情況下啟動你的應用,大大提高了可測試性萄焦。
注意:測試視圖是眾所周知的困難控轿,因為測試運行的小的包含的代碼塊。通常拂封,控制器會在依賴于其他應用程序狀態(tài)的場景中添加和配置視圖茬射。這意味著,意義上的小測試冒签,可以成為一個脆弱而繁瑣的命題在抛。
因此,你可能會想提出一個問題萧恕,如果只是View可以展現(xiàn)VIewModel刚梭,而ViewModel不能反過來展現(xiàn)View的話,那么ViewModel如何更新View呢票唆?啊哈F佣痢!這就是MVVM的秘訣了惰说!
MVVM和數(shù)據(jù)綁定(Data Binding)
MVVM模式依賴于數(shù)據(jù)綁定磨德,一個框架級的功能,自動連接對象屬性的用戶界面控件吆视。
有一個例子典挑,在微軟的WPF框架,下面一個例子將TextField的文本和ViewModel的Username綁定。
<TextField Text=”{DataBinding Path=Username,Mode=TwoWay}”/>
WPF的框架將這兩個成員變量“綁定”啦吧。
這個雙向的綁定確保了ViewModel的Username的改變同時TextFeild的文本也改變您觉,反之亦然,用戶的輸入也將改變ViewModel中的參數(shù)值授滓。
另一個例子琳水,基于web的流行的一個MVVM框架Knockout, 你可以發(fā)現(xiàn)兩個框架中數(shù)據(jù)綁定的相同的特點肆糕。
<input data-bind=”value: username”/>
上面的綁定將HTML的元素和JavaScript的模型綁定。
不幸的是在孝,iOS缺少一個數(shù)據(jù)綁定框架诚啃,但是這就是ReactiveCocoa所充當?shù)摹澳z水”作用。
具體從iOS開發(fā)的角度去看MVVM私沮,ViewController和它相關的UI——不論是xib始赎、storyboard或者是代碼組成的視圖(View):
ReactiveCocoa將兩者綁定起來。
注意:對于UI的各種實現(xiàn)方式仔燕,我高度推薦Martin Fowler的GUI Architectures article造垛。
你學到了足夠的理論知識了嗎?如果沒有晰搀,請回頭去再看一遍五辽。當然,如果你學得夠好了外恕,那么現(xiàn)在是時候開始創(chuàng)造你自己的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實現(xiàn)建丧,打開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關聯(lián)到一起,記住View和ViewModel的關聯(lián)戈咳,因此就需要在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;
接下來實現(xiàn)init方法
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel {
self = [super init];
if (self ) {
_viewModel = viewModel;
}
return self;
}
注意:這是個弱引用(弱指針),View引用了ViewModel猎唁,但沒有擁有它效拭。
在 viewDidLoad的最后加上下面代碼
[self bindViewModel];
下面是這個方法的實現(xiàn)
- (void)bindViewModel {
self.title = self.viewModel.title;
self.searchTextField.text = self.viewModel.searchText;
}
上面的代碼將會在UI初始化和ViewModel狀態(tài)在VIew上應用的時候運行。
最后一步是實例化ViewModel胖秒,并在View中應用缎患。
在viewDidLoad中添加以下
#import "RWTFlickrSearchViewModel.h"
加一個私有變量(在文件頂部的類擴展名內)。
@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel;
你會發(fā)現(xiàn)已經(jīng)有了一個createInitialViewController方法阎肝,更新他的實現(xiàn)方法:
- (UIViewController *)createInitialViewController {
self.viewModel = [RWTFlickrSearchViewModel new];
return [[RWTFlickrSearchViewController alloc]initWithViewModel:self.viewModel];
}
這將創(chuàng)建一個新的ViewModel實例挤渔,然后它返回View。這是應用程序的導航控制器的初始視圖风题。
恭喜你判导,這是你的第一個ViewModel。我得請你控制住你的興奮沛硅!這里還有很多要學習眼刃。
你可能已經(jīng)注意到了你沒有使用任何ReactiveCocoa呢。在其目前的形式摇肌,任何用戶進入搜索文本字段將不會反映在ViewModel擂红。
檢測有效搜索狀態(tài)
在這一部分中,你將使用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昵骤,它是一個信號,每次文本字段更新時肯适,將發(fā)出包含當前文本的一個事件变秦,RAC宏是一個綁定,上面代碼更新了ViewModel中的searchText對象的值框舔,它隨著rac_textSignal響應蹦玫。
總之,上面的代碼保證了searchText的值總是UI中的最新的值刘绣。如果上面的寫法讓你感到陌生樱溉,你真的應該重新學習一下ReactiveCocoa tutorials這個教程!
如果用戶輸入的文本是有效的额港,則只能啟用搜索按鈕饺窿。這里的輸入規(guī)則是,他們必須輸入超過三個字符移斩,然后才能執(zhí)行搜索肚医。
在 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輸入一些文字。每次文本在有效或無效狀態(tài)之間轉換時肠套,都會看到日志消息:
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宏創(chuàng)建了一個ViewModel中的 searchText的信號舰涌。map操作將文本流轉換為真值和假值。最后你稚, distinctuntilchanges是用來確保該信號只在狀態(tài)變化的時候傳遞值瓷耙。