【譯】MVVM Tutorial with ReactiveCocoa: Part 1/2

本文由Colin Eberhardt發表於raywenderlich,原文可查看MVVM Tutorial with ReactiveCocoa: Part ½html

你可能已經在Twitter上聽過這個這個笑話了:react

「iOS Architecture, where MVC stands for Massive View Controller」git

固然這在iOS開發圈內,這是個輕鬆的笑話,但我敢肯定你大實踐中遇到過這個問題:即視圖控制器太大且難以管理。github

這篇文章將介紹另外一種構建應用程序的模式—MVVM(Model-View-ViewModel)。經過結合ReactiveCocoa便利性,這個模式提供了一個很好的代替MVC的方案,它保證了讓視圖控制器的輕量性。objective-c

在本文我,咱們將經過構建一個簡單的Flickr查詢程序來一步步瞭解MVVM,這個程序的效果圖以下所示:json

image

在開始寫代碼以前,咱們先來了解一些基本的原理。設計模式

原文簡要介紹了一下ReactiveCocoa,在此再也不翻譯,能夠查看如下兩篇譯文:網絡

ReactiveCocoa Tutorial – The Definitive Introduction: Part ½框架

ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2異步

MVVM模式介紹

正如其名稱一下,MVVM是一個UI設計模式。它是MV*模式集合中的一員。MV*模式還包含MVC(Model View Controller)MVP(Model View Presenter)等。這些模式的目的在於將UI邏輯與業務邏輯分離,以讓程序更容易開發和測試。爲了更好的理解MVVM模式,咱們能夠看看其來源。

MVC是最初的UI設計模式,最先出如今Smalltalk語言中。下圖展現了MVC模式的主要組成:

image

這個模式將UI分紅Model(表示程序狀態)、View(由UI控件組成)、Controller(處理用戶交互與更新model)。MVC模式的最大問題是其使人至關困惑。它的概念看起來很好,但當咱們實現MVC時,就會產生上圖這種Model-View-Controller之間的環狀關係。這種相互關係將會致使可怕的混亂。

最近Martin Fowler介紹了MVC模式的一個變種,這種模式命名爲MVVM,並被微軟普遍採用並推廣。

image

這個模式的核心是ViewModel,它是一種特殊的model類型,用於表示程序的UI狀態。它包含描述每一個UI控件的狀態的屬性。例如,文本輸入域的當前文本,或者一個特定按鈕是否可用。它一樣暴露了視圖能夠執行哪些行爲,如按鈕點擊或手勢。

咱們能夠將ViewModel看做是視圖的模型(model-of-the-view)。MVVM模式中的三部分比MVC更加簡潔,下面是一些嚴格的限制

  1. View引用了ViewModel,但反過來不行。
  2. ViewModel引用了Model,但反過來不行。

若是咱們破壞了這些規則,便沒法正確地使用MVVM

這個模式有如下一些立竿見影的優點:

  1. 輕量的視圖:全部的UI邏輯都在ViewModel中。
  2. 便於測試:咱們能夠在沒有視圖的狀況下運行整個程序,這樣大大地增長了它的可測試性。

如今你可能注意到一個問題。若是View引用了ViewModel,但ViewModel沒有引用View,那ViewModel如何更新視圖呢?哈哈,這就得靠MVVM模式的私密武器了。

MVVM和數據綁定

MVVM模式依賴於數據綁定,它是一個框架級別的特性,用於自動鏈接對象屬性和UI控件。例如,在微軟的WPF框架中,下面的標籤將一個TextFieldText屬性綁定到ViewModelUsername屬性中。

1
<TextField Text=」{DataBinding Path=Username, Mode=TwoWay}」/> 

WPF框架將這兩個屬性綁定到一塊兒。

不過惋惜的是,iOS沒有數據綁定框架,幸運的是咱們能夠經過ReactiveCocoa來實現這一功能。咱們從iOS開發的角度來看看MVVM模式,ViewController及其相關的UI(nibstroyboard或純代碼的View)組成了View:

image

……而ReactiveCocoa綁定了ViewViewModel

理論講得差很少了,咱們能夠開始新的歷程了。

啓動項目結構

能夠從FlickrSearchStarterProject.zip中下載啓動項目。咱們使用Cocoapods來管理第三方庫,在對應目錄下執行pod install命令生成依賴庫後,咱們就能夠打開生成的RWTFlickrSearch.xcworkspace來運行咱們的項目了,初始運行效果以下圖:

image

咱們行熟悉下工程的結構:

image

ModelViewModel分組目前是空的,咱們會慢慢往裏面添加東西。View分組包含如下幾個類

  1. RWTFlickSearchViewController:程序的主屏幕,包含一個搜索輸入域和一個GO按鈕。
  2. RWTRecentSearchItemTableViewCell:用於在主頁中顯示搜索結果的table cell
  3. RWTSearchResultsViewController:搜索結果頁,顯示來自Flickrtableview
  4. RWTSearchResultsTableViewCell:渲染來自Flickr的單個圖片的table cell

如今來寫咱們的第一個ViewModel吧。

第一個ViewModel

ViewModel分組中添加一個繼承自NSObject的新類RWTFlickrSearchViewModel。而後在該類的頭文件中,添加如下兩行代碼:

1
2 
@property (nonatomic, strong) NSString *searchText; @property (nonatomic, strong) NSString *title; 

searchText屬性表示文本域中顯示文本,title屬性表示導航條上的標題。

打開RWTFlickrSearchViewModel.m文件添加如下代碼:

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 
@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的初始狀態。

接下來咱們將鏈接ViewModelView。記住View保存了一個ViewModel的引用。在這種狀況下,添加一個給定ViewModel的初始化方法來構造View是頗有必要的。打開RWTFlickrSearchViewController.h,並導入ViewModel頭文件:

1
#import "RWTFlickrSearchViewModel.h" 

並添加如下初始化方法:

1
2 3 4 5 
@interface RWTFlickrSearchViewController : UIViewController  - (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel;  @end 

RWTFlickrSearchViewController.m中,在類的擴展中添加如下私有屬性:

1
@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel; 

而後添加如下方法:

1
2 3 4 5 6 7 8 9 10 11 
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel {  self = [super init];   if (self)  {  _viewModel = viewModel;  }   return self; } 

這就在view中存儲了一個到ViewModel的引用。注意這是一個弱引用,這樣View引用了ViewModel,但沒有擁有它。

接下來在viewDidLoad裏面添加下面代碼:

1
[self bindViewModel]; 

該方法的實現以下:

1
2 3 4 5 
- (void)bindViewModel {  self.title = self.viewModel.title;  self.searchTextField.text = self.viewModel.searchText; } 

最後咱們須要建立ViewModel,並將其提供給View。在RWTAppDelegate.m中,添加如下頭文件:

1
#import "RWTFlickrSearchViewModel.h" 

同時添加一個私有屬性:

1
@property (nonatomic, strong) RWTFlickrSearchViewModel *viewModel; 

咱們會發現這個類中已以有一個createInitialViewController方法了,咱們用如下代碼來更新它:

1
2 3 4 
- (UIViewController *)createInitialViewController {  self.viewModel = [RWTFlickrSearchViewModel new];  return [[RWTFlickrSearchViewController alloc] initWithViewModel:self.viewModel]; } 

這個方法建立了一個ViewModel實例,而後構造並返回了View。這個視圖做程序導航控制器的初始視圖。

運行後的狀態以下:

image

這樣咱們就獲得了第一個ViewModel。不過仍然有許多東西要學的。你可能已經發現了咱們尚未使用ReactiveCocoa。到目前爲止,用戶在輸入框上的輸入操做不會影響到ViewModel

檢測可用的搜索狀態

如今,咱們來看看如何用ReactiveCocoa來綁定ViewModelView,以將搜索輸入框和按鈕鏈接到ViewModel

RWTFlickrSearchViewController.m中,咱們使用以下代碼更新bindViewModel方法。

1
2 3 4 5 
- (void)bindViewModel {  self.title = self.viewModel.title;  RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal; } 

ReactiveCocoa中,使用了分類將rac_textSignal屬性添加到UITextField類中。它是一個信號,在文本域每次更新時會發送一個包含當前文本的next事件。

RAC是一個用於作綁定操做的宏,上面的代碼會使用rac_textSignal發出的next信號來更新viewModelsearchText屬性。

搜索按鈕應該只有在用戶輸入有效時纔可點擊。爲了方便起見,咱們以輸入字符大於3時輸入有效爲準。在RWTFlickrSearchViewModel.m中導入如下頭文件。

1
#import <ReactiveCocoa/ReactiveCocoa.h> 

而後更新初始化方法:

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 
- (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);  }]; } 

運行程序並在輸入框中輸入一些字符,在控制檯中咱們能夠看到如下輸出:

1
2 3 
2014-08-07 21:50:44.078 RWTFlickrSearch[3116:60b] search text is valid 0 2014-08-07 21:50:59.493 RWTFlickrSearch[3116:60b] search text is valid 1 2014-08-07 21:51:02.594 RWTFlickrSearch[3116:60b] search text is valid 0 

上面的代碼使用RACObserve宏來從ViewModelsearchText屬性建立一個信號。map操做將文本轉化爲一個truefalse值的流。

最後,distinctUntilChanges確保信號只有在狀態改變時才發出值。

到目前爲止,咱們能夠看到ReactiveCocoa被用於將綁定View綁定到ViewModel,確保了這二者是同步的。另進一步地,ViewModel內部的ReactiveCocoa代碼用於觀察本身的狀態及執行其它操做。

這就是MVVM模式的基本處理過程。ReactiveCocoa一般用於綁定ViewViewModel,但在程序的其它層也很是有用。

添加搜索命令

本節將上面建立的validSearchSignal來建立綁定到View的操做。打開RWTFlickrSearchViewModel.h並添加如下頭文件

1
#import <ReactiveCocoa/ReactiveCocoa.h> 

同時添加如下屬性

1
@property (strong, nonatomic) RACCommand *executeSearch; 

RACCommandReactiveCocoa中用於表示UI操做的一個類。它包含一個表明了UI操做的結果的信號以及標識操做當前是否被執行的一個狀態。

RWTFlickrSearchViewModel.minitialize方法的最後添加如下代碼:

1
2 3 4 
self.executeSearch = [[RACCommand alloc] initWithEnabled:validSearchSignal  signalBlock:^RACSignal *(id input) {  return [self executeSearchSignal];  }]; 

這建立了一個在validSearchSignal發送true時可用的命令。另外,須要在下面實現executeSearchSignal方法,它提供了命令所執行的操做。

1
2 3 4 
- (RACSignal *)executeSearchSignal {  return [[[[RACSignal empty] logAll] delay:2.0] logAll]; } 

在這個方法中,咱們執行一些業務邏輯操做,以做爲命令執行的結果,並經過信號異步返回結果。

到目前爲止,上述代碼只提供了一個簡單的實現:空信號會當即完成。delay操做會將其所接收到的nextcomplete事件延遲兩秒執行。

最後一步是將這個命令鏈接到View中。打開RWTFlickrSearchViewController.m並在bindViewModel方法的結尾中添加如下代碼:

1
self.searchButton.rac_command = self.viewModel.executeSearch; 

rac_command屬性是UIButtonReactiveCocoa分類中添加的屬性。上面的代碼確保點擊按鈕執行給定的命令,且按鈕的可點擊狀態反應了命令的可用狀態。

運行代碼,輸入一些字符並點擊GO,獲得以下結果:

image

能夠看到,當輸入有效點擊按鈕時,按鈕會置灰2秒鐘,當執行的信號完成時又可點擊。咱們能夠看下控制檯的輸出,能夠發現空信號會當即完成,而延遲操做會在2秒後發出事件:

1
2 
2014-08-07 22:21:25.128 RWTFlickrSearch[3161:60b] <RACDynamicSignal: 0x17005ba20> name: +empty completed 2014-08-07 22:21:27.329 RWTFlickrSearch[3161:60b] <RACDynamicSignal: 0x17005dd30> name: [+empty] -delay: 2.000000 completed 

是否是很酷?

綁定、綁定仍是綁定

RACCommand監聽了搜索按鈕狀態的更新,但處理activity indicator的可見性則由咱們負責。RACCommand暴露了一個executing屬性,它是一個信號,發送truefalse來標明命令開始和結束執行的時間。咱們能夠用這個來影響當前命令的狀態。

RWTFlickrSearchViewController.m中的bindViewModel方法結尾處添加如下代碼:

1
RAC([UIApplication sharedApplication], networkActivityIndicatorVisible) = self.viewModel.executeSearch.executing; 

這將UIApplicationnetworkActivityIndicatorVisible屬性綁定到命令的executing信號中。這確保了無論命令何時執行,狀態欄中的網絡activity indicator都會顯示。

接下來添加如下代碼:

1
RAC(self.loadingIndicator, hidden) = [self.viewModel.executeSearch.executing not]; 

當命令執行時,應該隱藏加載indicator。這能夠經過not操做來反轉信號。

最後,添加如下代碼:

1
2 3 
[self.viewModel.executeSearch.executionSignals subscribeNext:^(id x) {  [self.searchTextField resignFirstResponder]; }]; 

這段代碼確保命令執行時隱藏鍵盤。executionSignals屬性發送由命令每次執行時生成的信號。這個屬性是信號的信號(見ReactiveCocoa Tutorial – The Definitive Introduction: Part ½)。當建立和發出一個新的命令執行信號時,隱藏鍵盤。

運行程序看看效果如何吧。

Model在哪?

到目前爲止,咱們已經有了一個清晰的View(RWTFlickrSearchViewController)ViewModel(RWTFlickrSearchViewModel),可是Model在哪呢?

答案很簡單:沒有!

當前的程序執行一個命令來響應用戶點擊搜索按鈕的操做,可是實現不作任何值的處理。ViewModel真正須要作的是使用當前的searchText來搜索Flickr,並返回一個匹配的列表。

咱們應該能夠直接在ViewModel添加業務邏輯,但相信我,你不但願這麼作。若是這是一個viewcontroller,我打賭你必定會直接這麼作。

ViewModel暴露屬性來表示UI狀態,它一樣暴露命令來表示UI操做(一般是方法)。ViewModel負責管理基於用戶交互的UI狀態的改變。然而它不負責實際執行這些交互產生的的業務邏輯,那是Model的工做。

接下來,咱們將在程序中添加Model層。

Model分組中,添加RWTFlickrSearch協議並提供如下實現

1
2 3 4 5 6 7 
#import <ReactiveCocoa/ReactiveCocoa.h>  @protocol RWTFlickrSearch <NSObject>  - (RACSignal *)flickrSearchSignal:(NSString *)searchString;  @end 

這個協議定義了Model層的初始接口,並將搜索Flickr的責任移出ViewModel

接下來在Model分組中添加RWTFlickrSearchImpl類,其繼承自NSObject,並實現了RWTFlickrSearch協議,以下代碼所示:

1
2 3 4 5 
#import "RWTFlickrSearch.h"  @interface RWTFlickrSearchImpl : NSObject <RWTFlickrSearch>  @end 

打開RWTFlickrSearchImpl.m文件,提供如下實現:

1
2 3 4 5 6 7 8 
@implementation RWTFlickrSearchImpl  - (RACSignal *)flickrSearchSignal:(NSString *)searchString {  return [[[[RACSignal empty] logAll] delay:2.0] logAll]; }  @end 

看着是否是有點眼熟?沒錯,咱們在上面的ViewModel中有相同的實現。

接下來咱們須要在ViewModel層中使用Model層。在ViewModel分組中添加RWTViewModelServices協議並以下實現:

1
2 3 4 5 
#import "RWTFlickrSearch.h"  @protocol RWTViewModelServices <NSObject> - (id<RWTFlickrSearch>)getFlickrSearchService; @end 

這個協議定義了惟一的一個方法,以容許ViewModel獲取一個引用,以指向RWTFlickrSearch協議的實現對象。

打開RWTFlickrSearchViewModel.h並導入頭文件

1
#import "RWTViewModelServices.h" 

更新初始化方法並將RWTViewModelServices做爲一個參數:

1
- (instancetype)initWithServices:(id<RWTViewModelServices>)services; 

RWTFlickrSearchViewModel.m中,添加類的分類並提供一個私有屬性來維護一個到RWTViewModelServices的引用:

1
2 3 
@interface RWTFlickrSearchViewModel () @property (nonatomic, weak) id<RWTViewModelServices> services; @end 

在該文件下面,添加初始化方法的實現:

1
2 3 4 5 6 7 8 9 10 11 12 
- (instancetype)initWithServices:(id<RWTViewModelServices>)services {  self = [super init];   if (self)  {  _services = services;  [self initialize];  }   return self; } 

這只是簡單的存儲了services的引用。

最後,更新executeSearchSignal方法:

1
2 3 4 
- (RACSignal *)executeSearchSignal {  return [[self.services getFlickrSearchService] flickrSearchSignal:self.searchText]; } 

最後是鏈接ModelViewModel

在工程的根分組中,添加一個NSObject的子類RWTViewModelServicesImpl。打開RWTViewModelServicesImpl.h並實現RWTViewModelServices協議:

1
2 3 4 
#import "RWTViewModelServices.h"  @interface RWTViewModelServicesImpl : NSObject <RWTViewModelServices> @end 

打開RWTViewModelServicesImpl.m,並添加實現:

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 
#import "RWTFlickrSearchImpl.h"  @interface RWTViewModelServicesImpl ()  @property (strong, nonatomic) RWTFlickrSearchImpl *searchService;  @end  @implementation RWTViewModelServicesImpl  - (instancetype)init {  if (self = [super init])  {  _searchService = [RWTFlickrSearchImpl new];  }   return self; }  - (id<RWTFlickrSearch>)getFlickrSearchService {  return self.searchService; }  @end 

這個類簡單建立了一個RWTFlickrSearchImpl實例,用於Model層搜索Flickr服務,並將其提供給ViewModel的請求。

最後,在RWTAppDelegate.m中添加如下頭文件

1
#import "RWTViewModelServicesImpl.h" 

並添加一個新的私有屬性

1
@property (nonatomic, strong) RWTViewModelServicesImpl *viewModelServices; 

再更新createInitialViewController方法:

1
2 3 4 5 
- (UIViewController *)createInitialViewController {  self.viewModelServices = [RWTViewModelServicesImpl new];  self.viewModel = [[RWTFlickrSearchViewModel alloc] initWithServices:self.viewModelServices];  return [[RWTFlickrSearchViewController alloc] initWithViewModel:self.viewModel]; } 

運行程序,驗證程序有沒有按以前的方式來工做。固然,這不是最有趣的變化,不過,能夠看看新代碼的形狀了。

Model層暴露了一個ViewModel層使用的’服務’。一個協議定義了這個服務的接口,提供了鬆散的組合。

咱們可使用這種方式來爲單元測試提供一個相似的服務實現。程序如今有了正確的MVVM結構,讓咱們小結一下:

  1. Model層暴露服務並負責提供程序的業務邏輯實現。
  2. ViewModel層表示程序的視圖狀態(view-state)。同時響應用戶交互及來自Model層的事件,二者都受view-state變化的影響。
  3. View層很薄,只提供ViewModel狀態的顯示及輸出用戶交互事件。

搜索Flickr

咱們繼續來完成Flickr的搜索實現,事情變得愈來愈有趣了。

首先咱們建立表示搜索結果的模型對象。在Model分組中,添加RWTFlickrPhoto類,併爲其添加三個屬性。

1
2 3 4 5 6 7 
@interface RWTFlickrPhoto : NSObject  @property (nonatomic, strong) NSString *title; @property (nonatomic, strong) NSURL *url; @property (nonatomic, strong) NSString *identifier;  @end 

這個模型對象表示由Flickr搜索API返回一個圖片。

打開RWTFlickrPhoto.m,並添加如下描述方法的實現:

1
2 3 4 
- (NSString *)description {  return self.title; } 

接下來,新建一個新的模型對象類RWTFlickrSearchResults,並添加如下屬性:

1
2 3 4 5 6 7 
@interface RWTFlickrSearchResults : NSObject  @property (strong, nonatomic) NSString *searchString; @property (strong, nonatomic) NSArray *photos; @property (nonatomic) NSInteger totalResults;  @end 

這個類表示由Flickr搜索返回的照片集合。

是時候實現搜索Flickr了。打開RWTFlickrSearchImpl.m並導入如下頭文件:

1
2 3 4 
#import "RWTFlickrSearchResults.h" #import "RWTFlickrPhoto.h" #import <objectiveflickr/ObjectiveFlickr.h> #import <LinqToObjectiveC/NSArray+LinqExtensions.h> 

而後添加如下類擴展:

1
2 3 4 5 6 
@interface RWTFlickrSearchImpl () <OFFlickrAPIRequestDelegate>  @property (strong, nonatomic) NSMutableSet *requests; @property (strong, nonatomic) OFFlickrAPIContext *flickrContext;  @end 

這個類實現了OFFlickrAPIRequestDelegate協議,並添加了兩個私有屬性。咱們會很快看到如何使用這些值。

繼續添加代碼:

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 
- (instancetype)init {  self = [super init];   if (self)  {  NSString *OFSampleAppAPIKey = @"YOUR_API_KEY_GOES_HERE";  NSString *OFSampleAppAPISharedSecret = @"YOUR_SECRET_GOES_HERE";   _flickrContext = [[OFFlickrAPIContext alloc] initWithAPIKey:OFSampleAppAPIKey sharedSecret:OFSampleAppAPISharedSecret];   _requests = [NSMutableSet new];  }   return self; } 

這段代碼建立了一個Flickr的上下文,用於存儲ObjectiveFlickr請求的數據。

當前Model層服務類提供的API有一個單獨的方法,用於查找基於文本搜索字符的圖片。不過咱們一會會添加更多的方法。

RWTFlickrSearchImpl.m中添加如下方法:

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 
- (RACSignal *)signalFromAPIMethod:(NSString *)method arguments:(NSDictionary *)args transform:(id (^)(NSDictionary *response))block {  // 1. 建立請求信號  return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {   // 2. 建立一個Flick請求對象  OFFlickrAPIRequest *flickrRequest = [[OFFlickrAPIRequest alloc] initWithAPIContext:self.flickrContext];  flickrRequest.delegate = self;  [self.requests addObject:flickrRequest];   // 3. 從代理方法中建立一個信號  RACSignal *successSignal = [self rac_signalForSelector:@selector(flickrAPIRequest:didCompleteWithResponse:)  fromProtocol:@protocol(OFFlickrAPIRequestDelegate)];   // 4. 處理響應  [[[successSignal  map:^id(RACTuple *tuple) {  return tuple.second;  }]  map:block]  subscribeNext:^(id x) {  [subscriber sendNext:x];  [subscriber sendCompleted];  }];   // 5. 開始請求  [flickrRequest callAPIMethodWithGET:method arguments:args];   // 6. 完成後,移除請求的引用  return [RACDisposable disposableWithBlock:^{  [self.requests removeObject:flickrRequest];  }];  }]; } 

這個方法須要傳入請求方法及請求參數,而後使用block參數來轉換響應對象。咱們重點看一下第4步:

1
2 3 4 5 6 7 8 9 10 11 12 
[[[successSignal  // 1. 從flickrAPIRequest:didCompleteWithResponse:代理方法中提取第二個參數  map:^id(RACTuple *tuple) {  return tuple.second;  }]  // 2. 轉換結果  map:block]  subscribeNext:^(id x) {  // 3. 將結果發送給訂閱者  [subscriber sendNext:x];  [subscriber sendCompleted];  }]; 

rac_signalForSelector:fromProtocol: 方法建立了successSignal,一樣也在代理方法的調用中建立了信號。

代理方法每次調用時,發出的next事件會附帶包含方法參數的RACTuple

實現Flickr搜索的最後一步以下:

1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 
- (RACSignal *)flickrSearchSignal:(NSString *)searchString {  return [self signalFromAPIMethod:@"flickr.photos.search"  arguments:@{@"text": searchString,  @"sort": @"interestingness-desc"}  transform:^id(NSDictionary *response) {   RWTFlickrSearchResults *results = [RWTFlickrSearchResults new];  results.searchString = searchString;  results.totalResults = [[response valueForKeyPath:@"photos.total"] integerValue];   NSArray *photos = [response valueForKeyPath:@"photos.photo"];  results.photos = [photos linq_select:^id(NSDictionary *jsonPhoto) {  RWTFlickrPhoto *photo = [RWTFlickrPhoto new];  photo.title = [jsonPhoto objectForKey:@"title"];  photo.identifier = [jsonPhoto objectForKey:@"id"];  photo.url = [self.flickrContext photoSourceURLFromDictionary:jsonPhoto  size:OFFlickrSmallSize];  return photo;  }];   return results;  }]; } 

上面的方法使用signalFromAPIMethod:arguments:transform:方法。flickr.photos.search方法提供的字典來搜索照片。

傳遞給transform參數的block簡單地將NSDictionary響應轉化爲一個等價的模型對象,讓它在ViewModel中更容易使用。

最後一步是打開RWTFlickrSearchViewModel.m方法,而後更新搜索信號來記錄日誌:

1
2 3 4 5 
- (RACSignal *)executeSearchSignal {  return [[[self.services getFlickrSearchService]  flickrSearchSignal:self.searchText]  logAll]; } 

編譯,運行並輸入一些字符後可在控制檯看到如下日誌:

1
2 3 4 5 6 7 8 
2014-06-03 [...] <RACDynamicSignal: 0x8c368a0> name: +createSignal: next: searchString=wibble, totalresults=1973, photos=(  "Wibble, wobble, wibble, wobble",  "unoa-army",  "Day 277: Cheers to the freakin' weekend!",  [...]  "Angry sky",  Nemesis ) 

這樣咱們MVVM指南的第一部分就差很少結束了,但在結束以前,讓咱們先看看內存問題吧。

內存管理

正如在ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2中所講的同樣,咱們在block中使用了self,這可能會致使循環引用的問題。而爲了不此問題,咱們須要使用@weakify@strongify宏來打破這種循環引用。

不過看看signalFromAPIMethod:arguments:transform:方法,你可能會迷惑爲何沒有使用這兩個宏來引用self?這是由於block是做爲createSignal:方法的一個參數,它不會在selfblock之間創建一個強引用關係。迷茫了吧?不相信的話只須要測試同樣這段代碼有沒有內存泄露就行。固然這時候就得用Instruments了,本身去看吧。哈哈。

何去何從?

例子工程的完整代碼能夠在這裏下載。在下一部分中,咱們將看看如何從ViewModel中初始化一個視圖控制器並實現更多的Flickr請求操做。

相關文章
相關標籤/搜索