- 繫結,繫結,繫結(重要的實情說三遍)
RACCommand能實時地更新search按鈕的狀態,但是時候來處理activity indicator的可見狀態了.
RACCommand擁有一個執行的屬性,它是用來表示命令開始和結束執行時反應真假事件的訊號量.你可以通過這個訊號量來反映程式中當前命令執行的狀態.
在RWTFlickrSearchViewController.m的bindViewModel的尾部新增:
1 |
RAC([UIApplication sharedApplication], networkActivityIndicatorVisible) = self.viewModel.executeSearch.executing; |
以上程式碼用來將UIApplication中的networkActivityIndicatorVisible屬性和命令執行訊號繫結.用來保證當命令執行時,小的網路啟用狀態標誌在status bar裡顯示.
下一步,新增:
1 |
RAC(self.loadingIndicator, hidden) = [self.viewModel.executeSearch.executing not]; |
當指令執行後,載入標誌將會被隱藏;這和你剛剛繫結的屬性相反.
ReactiveCocoa已經為我們提供了不執行的相反的訊號.最後,新增如下程式碼:
1 2 3 4 |
[self.viewModel.executeSearch.executionSignals subscribeNext:^(id x) { [self.searchTextField resignFirstResponder]; }]; |
上面的程式碼用來保證當命令執行時鍵盤會隱藏.executionSignals屬性用來在命令執行時實時地發出訊號.
這是個signals中的一個signal屬性(前面教程裡有介紹).當一個新的命令執行的時候就會被建立和執行,隱藏鍵盤.
執行程式,來驗證以上程式碼的執行.
- Model呢?
到現在為止,你已經定義了一個View(RWTFlickrSearchViewController)和ViewModel(RWTFlickrSearchViewModel),但是,怎麼木有Model呢?
答案很簡單:就是還沒有啊!
當前app使用者點選搜尋按鈕後就會執行命令,但卻沒有實現什麼.
我們需要實現的是利用當前輸入的搜尋文字通過ViewModel在Flickr進行搜尋,繼而返回相匹配的圖片列表.
你可以將此邏輯直接放在ViewModel裡,但相信我,你會後悔的!如果是個view controller,我到時強烈你這麼做.
View Model擁有UI狀態的屬性,而且還能夠執行命令(經常為UI上的動作方法).通過使用者的互動來管理改變UI狀態.
然而,並不表示這些互動實際的業務邏輯應該在View Model裡面.而這應該是Model的工作.
下一步,將會給應用增加Model層.
在Model group裡新增一個名為RWTFlickrSearch的新協議並提供瞭如下的方法:
1 2 3 4 5 6 7 8 |
#import <ReactiveCocoa/ReactiveCocoa.h> @ import Foundation; @protocol RWTFlickrSearch <NSObject> - (RACSignal *)flickrSearchSignal:(NSString *)searchString; @end |
這個協議定義了Model層的初始方法,用來將負責搜尋Flickr的任務從ViewModel裡移出.
接下來,在同一group裡建立一個名為RWTFlickrSearchImpl的NSObject的子類.並使其遵從剛才的協議:
1 2 3 4 5 6 |
@ import Foundation; #import "RWTFlickrSearch.h" @interface RWTFlickrSearchImpl : NSObject <RWTFlickrSearch> @end |
在RWTFlickrSearchImpl.m裡新增以下程式碼:
1 2 3 4 5 6 7 8 9 10 |
@implementation RWTFlickrSearchImpl - (RACSignal *)flickrSearchSignal:(NSString *)searchString { return [[[[RACSignal empty] logAll] delay:2.0] logAll]; } @end |
是不是覺得似曾相識?如果是的,那是因為這個同一’虛擬’的實現曾經位於ViewModel裡.
下一步是要在ViewModel裡使用Model層.在ViewModel group裡新增一個名為RWTViewModelServices的新協議:
1 2 3 4 5 6 7 8 |
@ import Foundation; #import "RWTFlickrSearch.h" @protocol RWTViewModelServices <NSObject> - (id<RWTFlickrSearch>) getFlickrSearchService; @end |
這個協議定義了ViewModel獲得對RWTFlickrSearch協議引用的方法.
在RWTFlickrSearchViewModel.h匯入這個新協議:
1 |
#import "RWTViewModelServices.h" |
更新initializer來將它作為引數:
1 |
- (instancetype) initWithServices:(id<RWTViewModelServices>)services; |
在RWTFlickrSearchViewModel.m裡新增一個類擴充套件和一個私有屬性來儲存對view model services的引用:
1 2 3 4 5 |
@interface RWTFlickrSearchViewModel () @property (nonatomic, weak) id<RWTViewModelServices> services; @end |
在同一檔案裡更新initializer:
1 2 3 4 5 6 7 8 |
- (instancetype) initWithServices:(id<RWTViewModelServices>)services { self = [super init]; if (self) { _services = services; [self initialize]; } return self; } |
以上用來儲存對services的引用.
最後,更新executeSearchSignal方法:
1 2 3 4 |
- (RACSignal *)executeSearchSignal { <span class="hljs-keyword">return</span> [[<span class="hljs-keyword">self</span><span class="hljs-variable">.services</span> getFlickrSearchService] flickrSearchSignal:<span class="hljs-keyword">self</span><span class="hljs-variable">.searchText</span>]; } |
上面的方法代理了model來實現搜尋.
最後一步是將Model和ViewModel相連.
在RWTFlickrSearch專案的’root’ group裡新增一個名為RWTViewModelServiceImpl的NSObject的子類.在RWTViewModelServicesImpl.h裡新增遵從RWTViewModelServices協議:
1 2 3 4 5 6 |
@ import Foundation; #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 |
#import "RWTViewModelServicesImpl.h" #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 upon 請求.
最後,在RWTAppDelegate.m裡匯入:
1 |
#import "RWTViewModelServicesImpl.h" |
新增一個私有屬性:
1 |
@property (strong, nonatomic) RWTViewModelServicesImpl *viewModelServices; |
更新createInitialViewController方法:
1 2 3 4 5 6 7 |
- (UIViewController *)createInitialViewController { self.viewModelServices = [RWTViewModelServicesImpl new]; self.viewModel = [[RWTFlickrSearchViewModel alloc] initWithServices:self.viewModelServices]; return [[RWTFlickrSearchViewController alloc] initWithViewModel:self.viewModel]; } |
執行程式,確保程式和之前執行的結果相似.
這並不是最令人興奮的變化,但花點時間來看看新程式碼的”形狀”.
Model層展示了’service’的ViewModel consumes.協議定義了這個服務介面,提供鬆耦合.
你可以用這個假設的服務實現用於單元測試.應用現在已經有了正確地Model-View-ViewModel結構.來小結下:
- Model提供應用業務邏輯實現的服務.在本應用中,它提供了Flickr搜尋的服務.
- ViewModel層提供了應用的檢視狀態.它提供了使用者互動以及來自檢視變化後Model層的事件.
- View層非常瘦,提供了ViewModel狀態變化後在檢視層的展示.
- Flickr搜尋
本章節,你將編寫一個真實的Flickr搜尋實現,事情開始變得令人興奮了呢;]
第一步是建立一個搜尋結果的model.
在Model group裡新增一個名為RWTFlickrPhoto的NSObject的子類,在介面裡新增三個屬性:
1 2 3 4 5 6 7 |
@interface RWTFlickrPhoto : NSObject @property (strong, nonatomic) NSString *title; @property (strong, nonatomic) NSURL *url; @property (strong, nonatomic) NSString *identifier; @end |
這個Model物件相當於Flickr搜尋API所返回的單張圖片.
在RWTFlickrPhoto.m新增如下方法:
1 2 3 |
- (NSString *)description { return self.title; } |
這個方法可以在實現UI變化之前通過列印搜尋的結果來測試搜尋的實現.
接下來,新增另一個名為RWTFlickrSearchResults的NSObject子類的模型.在介面裡新增如下屬性:
1 2 3 4 5 6 7 8 9 |
@ import Foundation; @interface RWTFlickrSearchResults : NSObject @property (strong, nonatomic) NSString *searchString; @property (strong, nonatomic) NSArray *photos; @property (nonatomic) NSUInteger totalResults; @end |
用來儲存Flickr搜尋返回的圖片集合.
在RWTFlickrSearchResults.m的description方法裡新增:
1 2 3 4 |
- (NSString *)description { return [NSString stringWithFormat:@"searchString=%@, totalresults=%lU, photos=%@", self.searchString, self.totalResults, self.photos]; } |
現在開始編寫Flickr搜尋的程式碼嘍!
在RWTFlickrSearchImpl.m新增:
1 2 3 4 |
#import "RWTFlickrSearchResults.h" #import "RWTFlickrPhoto.h" #import <objectiveflickr/ObjectiveFlickr.h> #import <LinqToObjectiveC/NSArray+LinqExtensions.h> |
匯入了剛才你建立的Model,新增了一對CocoaPods新增的擴充套件依賴:
- ObjectiveFlickr:這是個用Objective-C API 實現的Flickr API.用來處理授權和解析API返回的結果.使用此庫比直接呼叫Flickr API要簡單.
- LingToObjectiveC:提供了流暢和豐富的查詢介面,過濾和轉換陣列和詞典.
仍然在RWTFlickrSearchImpl.m裡新增類的擴充套件:
1 2 3 4 5 6 |
@interface RWTFlickrSearchImpl () <OFFlickrAPIRequestDelegate> @property (strong, nonatomic) NSMutableSet *requests; @property (strong, nonatomic) OFFlickrAPIContext *flickrContext; @end |
緊接著新增如下initializer:
1 2 3 4 5 6 7 8 9 10 11 12 |
- (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 ‘context’來儲存API請求所需的ObjectiveFlickr資料.
你可以在Flickr App Garden來獲取Key.
ObjectiveFlickr API非常常規.你建立了一個API請求後返回結果成功或失敗是通過定義的OFFlickrAPIRequestDelegate方法來處理的.
當前API是通過你的Model層服務類即RWTFlickrSearch協議,有一個方法來通過文字搜尋字串來進行圖片搜尋的.
然而,待會你將新增一些另外的方法.
因此,你將要去從直接用通用的方法到使用這種基於代理的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 35 36 37 38 39 |
- (RACSignal *)signalFromAPIMethod:(NSString *)method arguments:(NSDictionary *)args transform:(id (^)(NSDictionary *response))block { // 1. Create a signal for this request return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { // 2. Create a Flick request object OFFlickrAPIRequest *flickrRequest = [[OFFlickrAPIRequest alloc] initWithAPIContext:self.flickrContext]; flickrRequest.delegate = self; [self.requests addObject:flickrRequest]; // 3. Create a signal from the delegate method RACSignal *successSignal = [self rac_signalForSelector:@selector(flickrAPIRequest:didCompleteWithResponse:) fromProtocol:@protocol(OFFlickrAPIRequestDelegate)]; // 4. Handle the response [[[successSignal map:^id(RACTuple *tuple) { return tuple.second; }] map:block] subscribeNext:^(id x) { [subscriber sendNext:x]; [subscriber sendCompleted]; }]; // 5. Make the request [flickrRequest callAPIMethodWithGET:method arguments:args]; // 6. When we are done, remove the reference to this request return [RACDisposable disposableWithBlock:^{ [self.requests removeObject:flickrRequest]; }]; }]; } |
這個方法提供了一個API請求,詳見方法名和傳遞的引數,繼而通過block來傳遞結果.你將很快來了解它是如何工作的.
解析此方法,這裡有許多內容.下面是每步的釋義:
- createSignal方法建立了一個新訊號.傳遞訊號的block方法可以讓你處理髮送下一步、錯誤或者事件的完成訊號.
- ObjectivewFlickr請求被建立,這個請求的引用被儲存在請求set裡.如果沒有這段程式碼,OFFlickrAPIRequest將不被保留.
- rac_signalForSelector:fromProtocol方法從表示Flickr API 請求完成後的的代理方法裡建立一個訊號.
- 該訊號被繫結,結果變換和被髮送的結果作為訊號被建立.
- ObjectiveFlickr API 請求被使用.
- 當訊號被釋放後,block確保Flickr請求的引用被移除,避免記憶體漏洞.
現在我們來看下步驟4的更多細節:
1 2 3 4 5 6 7 8 9 10 11 12 |
[[[successSignal // 1. Extract the second argument map:^id(RACTuple *tuple) { return tuple.second; }] // 2. transform the results map:block] subscribeNext:^(id x) { // 3. send the results to the subscribers [subscriber sendNext:x]; [subscriber sendCompleted]; }]; |
rac_signalForSelector:fromProtocol:方法建立了successSignal,而且它還從代理方法裡建立訊號.
當代理方法被呼叫時,下一個事件包含方法引數的RACTuple被髮出.接著執行以下步驟:
- 一個map操作提取從flickrAPIRequest:didCompleteWithResponse:的第二個引數,代理方法中的詞典結果.
- block傳遞給此方法一個結果引數.你將很快看到如何將詞典轉換為模型物件.
- 最後,傳遞的結果被髮送給下一個事件,這個訊號完成.
最後來實現Flickr搜搜結果方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
- (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 API方法搜尋圖片,將以詞典為格式.
傳遞到引數中的block簡單地轉換詞典結果為模型物件,使ViewModel更容易使用.
程式碼使用linq_select方法通過LingToObjectiveC新增陣列.提供了API的陣列傳送.
最後在RWTFlickrSearchViewModel.m裡更新搜尋訊號日誌結果:
1 2 3 4 5 |
- (RACSignal *)executeSearchSignal { return [[[self.services getFlickrSearchService] flickrSearchSignal:self.searchText] logAll]; } |
執行,輸入一個搜尋字元後在console裡檢視訊號的結果日誌:
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 ) |