本文由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
在開始寫代碼以前,咱們先來了解一些基本的原理。設計模式
原文簡要介紹了一下ReactiveCocoa
,在此再也不翻譯,能夠查看如下兩篇譯文:網絡
ReactiveCocoa Tutorial – The Definitive Introduction: Part ½框架
ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2異步
正如其名稱一下,MVVM
是一個UI
設計模式。它是MV*
模式集合中的一員。MV*
模式還包含MVC(Model View Controller)
、MVP(Model View Presenter)
等。這些模式的目的在於將UI邏輯與業務邏輯分離,以讓程序更容易開發和測試。爲了更好的理解MVVM
模式,咱們能夠看看其來源。
MVC
是最初的UI
設計模式,最先出如今Smalltalk
語言中。下圖展現了MVC
模式的主要組成:
這個模式將UI
分紅Model
(表示程序狀態)、View
(由UI控件組成)、Controller
(處理用戶交互與更新model
)。MVC
模式的最大問題是其使人至關困惑。它的概念看起來很好,但當咱們實現MVC
時,就會產生上圖這種Model-View-Controller
之間的環狀關係。這種相互關係將會致使可怕的混亂。
最近Martin Fowler
介紹了MVC
模式的一個變種,這種模式命名爲MVVM
,並被微軟普遍採用並推廣。
這個模式的核心是ViewModel
,它是一種特殊的model
類型,用於表示程序的UI
狀態。它包含描述每一個UI
控件的狀態的屬性。例如,文本輸入域的當前文本,或者一個特定按鈕是否可用。它一樣暴露了視圖能夠執行哪些行爲,如按鈕點擊或手勢。
咱們能夠將ViewModel
看做是視圖的模型(model-of-the-view
)。MVVM
模式中的三部分比MVC
更加簡潔,下面是一些嚴格的限制
View
引用了ViewModel
,但反過來不行。ViewModel
引用了Model
,但反過來不行。若是咱們破壞了這些規則,便沒法正確地使用MVVM
。
這個模式有如下一些立竿見影的優點:
ViewModel
中。如今你可能注意到一個問題。若是View
引用了ViewModel
,但ViewModel
沒有引用View
,那ViewModel
如何更新視圖呢?哈哈,這就得靠MVVM
模式的私密武器了。
MVVM
模式依賴於數據綁定,它是一個框架級別的特性,用於自動鏈接對象屬性和UI控件。例如,在微軟的WPF
框架中,下面的標籤將一個TextField
的Text
屬性綁定到ViewModel
的Username
屬性中。
1
|
<TextField Text=」{DataBinding Path=Username, Mode=TwoWay}」/> |
WPF框架將這兩個屬性綁定到一塊兒。
不過惋惜的是,iOS
沒有數據綁定框架,幸運的是咱們能夠經過ReactiveCocoa
來實現這一功能。咱們從iOS
開發的角度來看看MVVM
模式,ViewController
及其相關的UI
(nib
, stroyboard
或純代碼的View
)組成了View:
……而ReactiveCocoa
綁定了View
和ViewModel
。
理論講得差很少了,咱們能夠開始新的歷程了。
能夠從FlickrSearchStarterProject.zip中下載啓動項目。咱們使用Cocoapods
來管理第三方庫,在對應目錄下執行pod install
命令生成依賴庫後,咱們就能夠打開生成的RWTFlickrSearch.xcworkspace
來運行咱們的項目了,初始運行效果以下圖:
咱們行熟悉下工程的結構:
Model
和ViewModel
分組目前是空的,咱們會慢慢往裏面添加東西。View
分組包含如下幾個類
RWTFlickSearchViewController
:程序的主屏幕,包含一個搜索輸入域和一個GO
按鈕。RWTRecentSearchItemTableViewCell
:用於在主頁中顯示搜索結果的table cell
RWTSearchResultsViewController
:搜索結果頁,顯示來自Flickr
的tableview
RWTSearchResultsTableViewCell
:渲染來自Flickr
的單個圖片的table cell
。如今來寫咱們的第一個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
的初始狀態。
接下來咱們將鏈接ViewModel
到View
。記住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
。這個視圖做程序導航控制器的初始視圖。
運行後的狀態以下:
這樣咱們就獲得了第一個ViewModel
。不過仍然有許多東西要學的。你可能已經發現了咱們尚未使用ReactiveCocoa
。到目前爲止,用戶在輸入框上的輸入操做不會影響到ViewModel
。
如今,咱們來看看如何用ReactiveCocoa
來綁定ViewModel
和View
,以將搜索輸入框和按鈕鏈接到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
信號來更新viewModel
的searchText
屬性。
搜索按鈕應該只有在用戶輸入有效時纔可點擊。爲了方便起見,咱們以輸入字符大於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
宏來從ViewModel
的searchText
屬性建立一個信號。map
操做將文本轉化爲一個true
或false
值的流。
最後,distinctUntilChanges
確保信號只有在狀態改變時才發出值。
到目前爲止,咱們能夠看到ReactiveCocoa
被用於將綁定View
綁定到ViewModel
,確保了這二者是同步的。另進一步地,ViewModel
內部的ReactiveCocoa
代碼用於觀察本身的狀態及執行其它操做。
這就是MVVM
模式的基本處理過程。ReactiveCocoa
一般用於綁定View
和ViewModel
,但在程序的其它層也很是有用。
本節將上面建立的validSearchSignal
來建立綁定到View
的操做。打開RWTFlickrSearchViewModel.h
並添加如下頭文件
1
|
#import <ReactiveCocoa/ReactiveCocoa.h> |
同時添加如下屬性
1
|
@property (strong, nonatomic) RACCommand *executeSearch; |
RACCommand
是ReactiveCocoa
中用於表示UI
操做的一個類。它包含一個表明了UI
操做的結果的信號以及標識操做當前是否被執行的一個狀態。
在RWTFlickrSearchViewModel.m
的initialize
方法的最後添加如下代碼:
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
操做會將其所接收到的next
或complete
事件延遲兩秒執行。
最後一步是將這個命令鏈接到View
中。打開RWTFlickrSearchViewController.m
並在bindViewModel
方法的結尾中添加如下代碼:
1
|
self.searchButton.rac_command = self.viewModel.executeSearch; |
rac_command
屬性是UIButton
的ReactiveCocoa
分類中添加的屬性。上面的代碼確保點擊按鈕執行給定的命令,且按鈕的可點擊狀態反應了命令的可用狀態。
運行代碼,輸入一些字符並點擊GO
,獲得以下結果:
能夠看到,當輸入有效點擊按鈕時,按鈕會置灰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
屬性,它是一個信號,發送true
或false
來標明命令開始和結束執行的時間。咱們能夠用這個來影響當前命令的狀態。
在RWTFlickrSearchViewController.m
中的bindViewModel
方法結尾處添加如下代碼:
1
|
RAC([UIApplication sharedApplication], networkActivityIndicatorVisible) = self.viewModel.executeSearch.executing; |
這將UIApplication
的networkActivityIndicatorVisible
屬性綁定到命令的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 ½)。當建立和發出一個新的命令執行信號時,隱藏鍵盤。
運行程序看看效果如何吧。
到目前爲止,咱們已經有了一個清晰的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]; } |
最後是鏈接Model
和ViewModel
。
在工程的根分組中,添加一個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
結構,讓咱們小結一下:
ViewModel
層表示程序的視圖狀態(view-state
)。同時響應用戶交互及來自Model
層的事件,二者都受view-state
變化的影響。View
層很薄,只提供ViewModel
狀態的顯示及輸出用戶交互事件。咱們繼續來完成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:
方法的一個參數,它不會在self
和block
之間創建一個強引用關係。迷茫了吧?不相信的話只須要測試同樣這段代碼有沒有內存泄露就行。固然這時候就得用Instruments
了,本身去看吧。哈哈。
例子工程的完整代碼能夠在這裏下載。在下一部分中,咱們將看看如何從ViewModel
中初始化一個視圖控制器並實現更多的Flickr
請求操做。