MVVM在ReactiveCocoa下的實(shí)現(xiàn)

原文:MVVM Tutorial with ReactiveCocoa: Part 1/2

你可能之前在推特看過這么一個(gè)笑話:

*“iOS Architecture, where MVC stands for Massive View Controller” *

這是對(duì)iOS開發(fā)者的一個(gè)小小調(diào)侃棵譬,但我敢說(shuō)你們肯定都在實(shí)踐中遇到過這樣的問題:臃腫而又難于管理的視圖控制器呼股。

這篇教程將介紹一個(gè)構(gòu)造應(yīng)用的嶄新模式:Model-View-ViewModel(MVVM)廓推。這種得益于ReactiveCocoa的模式為我們提供了一個(gè)優(yōu)異的MVC替代方案惧蛹,讓視圖控制器更加有序和輕量囤萤。

在這篇MVVM的教程中,你將要構(gòu)建一個(gè)簡(jiǎn)單的Flickr搜索應(yīng)用,效果如下:


注意:這篇教程基于Objective-C,并非Swift封锉。同時(shí)開發(fā)環(huán)境使用的是Xcode 5,而非Xcode 6,所以所有的截圖都從Xcode 5中截取成福。為了得到最好的效果碾局,請(qǐng)使用Xcode 5來(lái)完成本教程。(譯注:因?yàn)槭?4年的文章奴艾,所以Xcode版本比較老舊净当,但具體界面操作還是大同小異)
但無(wú)路如何,如果你實(shí)在對(duì)這個(gè)示例項(xiàng)目的Swift版本感興趣的話蕴潦,你可以關(guān)注我最近的博客中的實(shí)現(xiàn)O裉洹(譯注:因?yàn)镾wift幾個(gè)版本以來(lái)API和語(yǔ)法更新較多,ReactiveCocoa對(duì)應(yīng)也有較大的更新潭苞,當(dāng)時(shí)的代碼估計(jì)已經(jīng)不再適用)

在編寫代碼之前忽冻,我們先來(lái)學(xué)習(xí)一點(diǎn)理論知識(shí)。

ReactiveCocoa的簡(jiǎn)短回顧

這篇教程是主要介紹MVVM此疹,并默認(rèn)你已經(jīng)掌握了ReactiveCocoa的相關(guān)知識(shí)點(diǎn)僧诚。如果你之前從來(lái)沒有使用過ReactiveCocoa,我強(qiáng)烈建議你閱讀一下早前相關(guān)的教程秀菱。

如果你需要對(duì)ReactiveCocoa進(jìn)行快速?gòu)?fù)習(xí)振诬,下面我會(huì)簡(jiǎn)短地回顧一些關(guān)鍵點(diǎn)。

ReactiveCocoa最核心的就是信號(hào)衍菱,對(duì)應(yīng)表現(xiàn)為RACSignal類赶么。信號(hào)發(fā)送事件流,而事件流又分三種類型:next脊串、completederror辫呻。

使用這種簡(jiǎn)單的模型嘉汰,ReactiveCocoa可以對(duì)代理模式, target-action模式谱净,鍵值對(duì)觀察(KVO)等進(jìn)行替代。

由于信號(hào)創(chuàng)建的接口更加同質(zhì)(homogenous)颅拦,所以代碼更加易讀缕坎。但ReactiveCocoa更強(qiáng)大的地方體現(xiàn)在對(duì)這些信號(hào)的高級(jí)操作怖侦。那讓你可以用相當(dāng)簡(jiǎn)潔的方式實(shí)現(xiàn)復(fù)雜的過濾,轉(zhuǎn)換和信號(hào)協(xié)調(diào)谜叹。

ReactiveCocoa在MVVM的實(shí)現(xiàn)中是一個(gè)特殊的存在匾寝。它是ViewModel和View的“粘合劑”。但這概念對(duì)現(xiàn)在的你來(lái)說(shuō)可能有點(diǎn)超前了……

MVVM的介紹

眾所周知荷腊,Model-View-ViewModel(MVVM)是一中UI設(shè)計(jì)模式艳悔。它是MV*模式眾多成員中的一份子,其他還包括Model View Controller (MVC)女仰,Model View Presenter (MVP)等猜年。

這些模式都關(guān)注于將UI邏輯從業(yè)務(wù)邏輯中分離出來(lái)抡锈,讓應(yīng)用更易于開發(fā)與測(cè)試。

注意:想了解更多常見的設(shè)計(jì)模式乔外,我推薦Eli或者Ash Furrow的文章床三。

為了更好理解MVVM模式,我們可以回顧一下它的起源袁稽。

MVC是第一個(gè)UI設(shè)計(jì)模式勿璃,它的起源可以追溯到二十世紀(jì)七十年代的Smalltalk語(yǔ)言。下圖展示了MVC模式的主要組成:

這個(gè)模式將UI視圖分解為三個(gè)部分:表示應(yīng)用狀態(tài)的Model推汽,UI控件組成的View补疑,和根據(jù)情況處理交互和更新模型的Controller。

而MVC模式最大的問題就是它迷惑性歹撒。這概念雖然看起來(lái)很美莲组,但當(dāng)人們開始實(shí)現(xiàn)MVC時(shí),Model暖夭,View和 Controller之間就會(huì)形成上圖中相似的循環(huán)關(guān)系锹杈。換言之,這導(dǎo)致了一種可怕的混亂迈着。

而最近Martin Fowler提出了一種稱為Presentation Model的MVC變體竭望,這種變體被微軟以MVVM為名,采納并普及開來(lái)裕菠。

這種模式的核心是ViewModel咬清,一種用以代表應(yīng)用UI界面的狀態(tài)的特殊模型。

其包含了每個(gè)UI控件的狀態(tài)屬性奴潘。例如旧烧,文本輸入框的實(shí)時(shí)內(nèi)容,或者指定按鈕的可用與否画髓。它也暴露了視圖的具體的可用操作掘剪,比如按鈕的點(diǎn)擊或手勢(shì)。

可以將ViewModel理解為視圖的模型(model-of-the-view)奈虾。

MVVM模式中三個(gè)組件間的關(guān)系比MVC中的更加簡(jiǎn)單夺谁,具體遵循這些嚴(yán)格的規(guī)定:

  1. View能引用ViewModel,但ViewModel不能引用View肉微。
  2. ViewModel能引用Model匾鸥,但Model不能引用ViewModel。

只要你違反了其中一條規(guī)定浪册,那么你就沒有正確的使用MVVM扫腺!

這種模式有兩個(gè)立竿見影的好處:

  1. 輕量級(jí)的視圖:所有的UI邏輯都遷移到ViewModel中岗照,剩下的視圖非常輕量村象。
  2. 方便測(cè)試:你可以脫離視圖運(yùn)行應(yīng)用邏輯笆环,顯著地提升了可測(cè)試性。

注意:眾所周知厚者,視圖測(cè)試相當(dāng)困難躁劣,因?yàn)槟呐率窃傩〉囊粋€(gè)測(cè)試都會(huì)涉及大量不相關(guān)的代碼。通常來(lái)說(shuō)库菲,控制器會(huì)對(duì)依賴于其他的應(yīng)用狀態(tài)的場(chǎng)景添加和設(shè)置視圖账忘。這意味著有效的小型測(cè)試將是一個(gè)脆弱而繁瑣的命題。

此時(shí)你可能會(huì)提出一個(gè)問題熙宇。如果View只能單向引用ViewModel鳖擒,ViewModel又如何更新View呢?

這正是MVVM模式的秘訣烫止。

MVVM和數(shù)據(jù)綁定

MVVM模式依賴于數(shù)據(jù)綁定這樣一個(gè)框架級(jí)的功能蒋荚,用以自動(dòng)連接對(duì)象屬性和UI控件。

舉個(gè)例子馆蠕,在微軟的WPF框架中期升,下面的標(biāo)簽綁定了文本框的Text屬性和ViewModel的Username屬性:

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

WPF框架讓這兩個(gè)屬性關(guān)聯(lián)在一起。

這種雙向綁定一方面讓Username的屬性改動(dòng)得以傳遞到文本框的Text屬性中互躬,而另一方面播赁,又能把用戶的輸入反映給ViewModel。

在另一個(gè)流行的網(wǎng)頁(yè)端MVVM框架Knockout中吼渡,你也可以看到相似的綁定操作:

<input data-bind=”value: username”/>

上面的代碼就把一個(gè)HTML元素的屬性與一個(gè)JavaScript對(duì)象綁定起來(lái)容为。

很不幸,iOS缺乏這樣一個(gè)數(shù)據(jù)綁定的框架诞吱,但這正是ReactiveCocoa發(fā)揮“粘合劑”作用的地方舟奠,將ViewModel進(jìn)行關(guān)聯(lián)。

現(xiàn)在特別從iOS開發(fā)的角度研究一下MVVM模式房维。我們能發(fā)現(xiàn)視圖控制器和它關(guān)聯(lián)的UI(無(wú)論是nib沼瘫,stroyboard還是通過代碼創(chuàng)建的)共同組成了View部分,而ReactiveCocoa則將View與ViewModel綁定起來(lái):

注意:想了解更多關(guān)于UI模式的歷史分析咙俩,我強(qiáng)烈推薦Martin Fowler的Martin Fowler’s GUI Architectures耿戚。

你是不是已經(jīng)受夠這些理論了?好吧阿趁,如果不是你可以往上再重讀一遍膜蛔。哦?你都已經(jīng)掌握了?好吧脖阵,那接下來(lái)是時(shí)候創(chuàng)建你自己的ViewModel了皂股。

初始的項(xiàng)目結(jié)構(gòu)

首先下載下面的初始項(xiàng)目:

項(xiàng)目使用CocoaPods去管理依賴包(如果你是第一次接觸CocoaPods,我們?yōu)槟銣?zhǔn)備了一個(gè)新手教程C)呜呐。執(zhí)行pod install命令去獲取依賴就斤,確認(rèn)你能看到下面的輸出:

譯注:由于當(dāng)時(shí)CocoaPods的版本較低,現(xiàn)今Podfile文件的格式有所改變蘑辑,更新如下:

platform :ios, '7.0'
target 'RWTFlickrSearch' do
    pod 'ReactiveCocoa', '2.1.8'
    pod 'objectiveflickr', '2.0.4'
    pod 'LinqToObjectiveC', '2.0.0'
    pod 'SDWebImage', '3.6'
end
$ 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

你會(huì)在接下來(lái)的使用中學(xué)到更多關(guān)于這些依賴包的的內(nèi)容洋机。

譯注:這個(gè)版本(3.6)的SDWebImage在Xcode6及后續(xù)版本有報(bào)錯(cuò),請(qǐng)?jiān)?code>@implementation SDWebImageDownloaderOperation {...}后添加:

@synthesize executing = _executing ;  
@synthesize finished = _finished;  

這個(gè)教程的初始工程已經(jīng)包含了用視圖控制器和nib文件實(shí)現(xiàn)的應(yīng)用視圖洋魂。打開由CocoaPods 生成的RWTFlickrSearch.xcworkspace文件绷旗,編譯運(yùn)行初始項(xiàng)目后你就能看到這些視圖中的一個(gè):

花點(diǎn)時(shí)間熟悉一下項(xiàng)目的架構(gòu):


Model和ViewModel分組中現(xiàn)在為空;你很快將往里面添加文件副砍。而View分組包含內(nèi)容如下:

  • RWTFlickSearchViewController:應(yīng)用的主界面衔肢,包含一個(gè)搜索文本框和“Go”按鈕。
  • RWTRecentSearchItemTableViewCell:在主屏幕中展示最新的搜索結(jié)果的列表單元格豁翎。
  • RWTSearchResultsViewController:搜索結(jié)果界面膀懈,展示來(lái)自Flickr的圖片列表。
  • RWTSearchResultsTableViewCell:展示單幅Flickr圖片的列表單元格

現(xiàn)在可以添加你的第一個(gè)ViewModel了谨垃!

你的第一個(gè)ViewModel

在ViewModel分組下添加一個(gè)新類启搂,命名為RWTFlickrSearchViewModel并繼承自NSObject

打開新添加的頭文件刘陶,并往里面添加如下兩個(gè)屬性:

@interface RWTFlickrSearchViewModel : NSObject
 
@property (strong, nonatomic) NSString *searchText;
@property (strong, nonatomic) NSString *title;
 
@end

searchText屬性代表著文本框顯示的文本胳赌,而title屬性則表示導(dǎo)航條中的顯示標(biāo)題。

注意:為了更容易理解應(yīng)用的架構(gòu)匙隔,View和ViewModel都共用一個(gè)只有后綴不同的名字疑苫。比如RWTFlickrSearch-ViewModelRWTFlickrSearch-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

這簡(jiǎn)單的設(shè)置了ViewModel的初始狀態(tài)纷责。

下一步是將ViewModel和View關(guān)聯(lián)起來(lái)捍掺。記得View持有ViewModel的引用。在這種情況下再膳,最好為View添加初始化方法挺勿,賦予對(duì)應(yīng)的ViewModel。

注意:在本教程中我們把控制器也視為“View”的一部分喂柒,以更符合MVVM中“View”的語(yǔ)義不瓶。這有別于UIKit中一貫的用法。

打開RWTFlickrSearchViewController.h并導(dǎo)入ViewModel的頭文件:

#import "RWTFlickrSearchViewModel.h"

然后添加初始方法如下:

@interface RWTFlickrSearchViewController : UIViewController
 
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel;
 
@end

RWTFlickrSearchViewController.m類擴(kuò)展的UI引用后添加私有屬性如下:

@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel;

然后添加初始方法如下:

- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel {
  self = [super init];
  if (self ) {
    _viewModel = viewModel;
  }
  return self;
}

這存儲(chǔ)了這個(gè)View對(duì)應(yīng)的ViewModel引用灾杰。

注意:這是一個(gè)弱引用蚊丐;View引用ViewModel,但并不持有它艳吠。

viewDidLoad方法的末端添加代碼如下:

[self bindViewModel];

然后實(shí)現(xiàn)這個(gè)方法如下:

- (void)bindViewModel {
  self.title = self.viewModel.title;
  self.searchTextField.text = self.viewModel.searchText;
}

上面的代碼會(huì)在UI初始化時(shí)執(zhí)行麦备,并把ViewModel的狀態(tài)賦予給View。

最后一步就是創(chuàng)建ViewModel的實(shí)例,并提供給View凛篙。

RWTAppDelegate.m添加引用如下:

#import "RWTFlickrSearchViewModel.h"

并添加私有屬性(在文件上方的類擴(kuò)展中):

@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel;

你會(huì)發(fā)現(xiàn)這個(gè)類中已經(jīng)有一個(gè)createInitialViewController方法弄屡,更新它的實(shí)現(xiàn)如下:

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

這創(chuàng)建了一個(gè)ViewModel的實(shí)例,然后構(gòu)造返回了View鞋诗。這就是應(yīng)用導(dǎo)航控制器的初始視圖。

編譯運(yùn)行迈嘹,現(xiàn)在視圖已經(jīng)具有了一些新的狀態(tài)削彬。


恭喜,這就是你的第一個(gè)ViewModel秀仲。但請(qǐng)先收斂一下你的興奮之情融痛,接下來(lái)還有很多東西要學(xué)呢 ; ]

你可能已經(jīng)發(fā)現(xiàn),你還沒有使用任何的ReactiveCocoa神僵。在現(xiàn)在的情況下雁刷,用戶在搜索文本框輸入的任何內(nèi)容都不會(huì)反映到ViewModel中。

檢測(cè)搜索有效性

在這個(gè)部分保礼,你將會(huì)使用ReactiveCocoa綁定ViewModel和View沛励,以便把搜索文本框和按鈕都與ViewModel關(guān)聯(lián)起來(lái)。

RWTFlickrSearchViewController.m更新bindViewModel方法如下:

- (void)bindViewModel {
  self.title = self.viewModel.title;
  RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
}

ReactiveCocoa使用category為UITextField添加了rac_textSignal屬性炮障。這個(gè)信號(hào)會(huì)在文本框更新時(shí)發(fā)送一個(gè)包含現(xiàn)有文本的next事件目派。

RAC宏是一個(gè)綁定操作。上面的代碼會(huì)在rac_textSignal發(fā)送的next事件時(shí)用里面的內(nèi)容更新viewModelsearchText屬性胁赢。

簡(jiǎn)單來(lái)說(shuō)企蹭,這保證了searchText總是反映著當(dāng)前的UI狀態(tài)。如果上面的內(nèi)容讓你覺得云里霧里智末,那么你真的需要復(fù)習(xí)一下之前的兩篇ReactiveCocoa教程了谅摄!

搜索按鈕應(yīng)該只有在用戶輸入的文本有效時(shí)才可用。為了簡(jiǎn)化流程系馆,我們暫時(shí)規(guī)定只有當(dāng)輸入的文本超過三個(gè)字節(jié)時(shí)才能執(zhí)行搜索送漠。

RWTFlickrSearchViewModel.m添加導(dǎo)入如下:

#import <ReactiveCocoa/ReactiveCocoa.h>

之后更新初始化方法如下:

- (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);
  }];
}

編譯運(yùn)行并在文本框中任意輸入一些內(nèi)容。現(xiàn)在當(dāng)文本內(nèi)容在有效和無(wú)效間轉(zhuǎn)換時(shí)你就能在控制臺(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宏為ViewModel的searchText屬性創(chuàng)建了一個(gè)信號(hào)(這是ReactiveCocoa對(duì)KVO的封裝)由蘑。隨后map操作將文本轉(zhuǎn)化為值為布爾值的流螺男。

最后,distinctUntilChanges方法用于保證該信號(hào)只有在狀態(tài)變更時(shí)才會(huì)發(fā)送新值纵穿。

注意:如果你在過程中感到困難下隧,嘗試將這個(gè)過程分解。先添加RACObserve宏谓媒,打印輸出淆院,然后再添加map操作和distinctUntilChanged方法。

至今ReactiveCocoa主要用于綁定View和ViewModel,以確保兩者保持同步土辩。而除此之外支救,ReactiveCocoa也在ViewModel的內(nèi)部用于觀察它自身的state和執(zhí)行其他操作。

這種模式在本教程隨處可見拷淘。ReactiveCocoa在View與ViewModel的綁定中至關(guān)重要的同時(shí)各墨,對(duì)應(yīng)用的其他層級(jí)起著巨大的作用。

添加搜索命令

在這個(gè)部分启涯,你會(huì)為validSearchSignal:方法添加更加實(shí)用的實(shí)現(xiàn)贬堵,用以創(chuàng)建視圖相關(guān)的命令。
打開RWTFlickrSearchViewModel.h并添加以下導(dǎo)入:

#import <ReactiveCocoa/ReactiveCocoa.h>

和屬性如下:

@property (strong, nonatomic) RACCommand *executeSearch;

RACCommand是ReactiveCocoa中一個(gè)表示UI操作的概念结洼。它包含UI操作的結(jié)果和當(dāng)前操作是否正在執(zhí)行的狀態(tài)黎做。

RWTFlickrSearchViewModel.m的初始化方法中添加代碼如下:

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

這創(chuàng)建了一個(gè)當(dāng)validSearchSignal發(fā)送true時(shí)有效的命令。

在同一文件的下松忍,添加用于生成命令中執(zhí)行信號(hào)的方法如下:

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

在這個(gè)方法中蒸殿,你將實(shí)現(xiàn)一些在命令執(zhí)行時(shí)處理的業(yè)務(wù)邏輯,并通過信號(hào)異步地返回結(jié)果鸣峭。

上面的方法中暫時(shí)還是假實(shí)現(xiàn)宏所,空的信號(hào)會(huì)立即結(jié)束。延遲操作為所有接受到的nextcompleted事件添加了兩秒的延遲摊溶。這是讓代碼執(zhí)行顯得更為真實(shí)的巧妙方法楣铁。

注意:上面的信號(hào)管道有兩個(gè)附加的logAll操作,那是副作用(side effects)的一種更扁,用于打印通過的所有事件盖腕。
這在測(cè)試ReactiveCocoa代碼時(shí)相當(dāng)有用,當(dāng)然如果你認(rèn)為它們并不必要的話浓镜,可以隨時(shí)移除掉這些打印操作溃列。

最后要將這個(gè)命令與視圖關(guān)聯(lián)起來(lái)。打開RWTFlickrSearchViewController.m并在bindViewModel方法的末端添加代碼如下:

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

rac_command是ReactiveCocoa對(duì)UIButton的擴(kuò)充屬性膛薛。上面的代碼讓按鈕在點(diǎn)擊時(shí)執(zhí)行對(duì)應(yīng)的命令听隐,并把按鈕的可用狀態(tài)與命令的可用狀態(tài)關(guān)聯(lián)起來(lái)。

編譯運(yùn)行哄啄,任意輸入一點(diǎn)文本后點(diǎn)擊“Go”:


你會(huì)看到按鈕只有在文本框中多與3個(gè)字符時(shí)有效雅任。而且,你點(diǎn)擊按鈕時(shí)咨跌,按鈕將在兩秒內(nèi)不可用沪么,直到執(zhí)行信號(hào)完成后才重新變?yōu)榭捎谩?/p>

而在控制臺(tái)中,你還可以看到空信號(hào)立馬結(jié)束锌半,延遲操作讓這事件在延遲兩秒后發(fā)出:

09:31:25.728 RWTFlickrSearch ... name: +empty completed
09:31:27.730 RWTFlickrSearch ... name: [+empty] -delay: 2.0 completed

綁定禽车,綁定和更多的綁定

RACCommand負(fù)責(zé)了搜索按鈕的狀態(tài)更新,而活動(dòng)指示器(activity indicator)的可見與否則需要你來(lái)處理。

RACCommand提供了一個(gè)executing屬性殉摔,這是一個(gè)發(fā)送true或false事件的信號(hào)州胳,用以表明命令是否正在執(zhí)行。你可以在應(yīng)用的其他地方使用這個(gè)屬性反映當(dāng)前命令的狀態(tài)逸月。

RWTFlickrSearchViewController.mbindViewModel的方法末端添加代碼如下:

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

這綁定了UIApplicationnetworkActivityIndicatorVisible屬性和命令的executing信號(hào)栓撞。這保證了每當(dāng)命令執(zhí)行時(shí),狀態(tài)欄中小型的網(wǎng)絡(luò)活動(dòng)指示器就會(huì)顯示碗硬。

然后繼續(xù)添加代碼如下:

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

當(dāng)命令執(zhí)行完成時(shí)瓤湘,加載指示符應(yīng)該隱藏起來(lái)。hiddenexecuting在邏輯上是剛好相反的肛响。

很幸運(yùn),ReactiveCocoa為這種情況提供了一個(gè)not方法去翻轉(zhuǎn)信號(hào)的結(jié)果惜索。

最后再添加代碼如下:

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

這保證了命令執(zhí)行時(shí)會(huì)把鍵盤隱藏起來(lái)特笋。executionSignals屬性發(fā)送每次命令執(zhí)行時(shí)生成的信號(hào)(譯注:即代碼中的x為命令生成的信號(hào))。

這屬性是一個(gè)信號(hào)中的信號(hào)(在早前的教程中介紹過)巾兆。即當(dāng)一個(gè)新的命令執(zhí)行信號(hào)被創(chuàng)建并發(fā)送猎物,鍵盤就會(huì)隱藏。

現(xiàn)在編譯運(yùn)行應(yīng)用角塑,實(shí)際檢驗(yàn)一下上面的代碼蔫磨。

Model在哪?

至今為止你已經(jīng)明確的定義了View(RWTFlickrSearchViewController)和ViewModel(RWTFlickrSearchViewModel)圃伶,但是搀罢,額唧取,Model在哪媒区?

答案非常簡(jiǎn)單:還沒有!

現(xiàn)在應(yīng)用會(huì)在用戶點(diǎn)擊搜索按鈕時(shí)執(zhí)行一個(gè)命令座掘,但具體內(nèi)容還沒實(shí)現(xiàn)。

需要做的是在ViewModel中用當(dāng)前的searchText屬性搜索Flickr咆霜,并返回一個(gè)匹配圖片的列表脉课。

你可以直接在ViewModel中添加這個(gè)邏輯视事,但相信我,你會(huì)后悔的!如果這是一個(gè)視圖控制器,我賭你一定就這么干了。

ViewModel提供了代表UI狀態(tài)的屬性和代表UI操作的命令(通常是方法)奶卓。其負(fù)責(zé)管理用戶交互導(dǎo)致的UI狀態(tài)變更眉睹。

但是殖卑,ViewModel不應(yīng)該處理因?yàn)榻换?zhí)行的具體的業(yè)務(wù)邏輯接校。那是Model的工作。

在下一步睦柴,你將在應(yīng)用中創(chuàng)建一個(gè)Model層诽凌。里面會(huì)包含一些“腳手架代碼”(scaffolding code),你只要跟著教程做坦敌,你很快就會(huì)發(fā)現(xiàn)更有趣的東西侣诵。

在Model分組下痢法,添加一個(gè)名為RWTFlickrSearch的協(xié)議,并實(shí)現(xiàn)如下:

#import <ReactiveCocoa/ReactiveCocoa.h>
@import Foundation;
 
@protocol RWTFlickrSearch <NSObject>
 
- (RACSignal *)flickrSearchSignal:(NSString *)searchString;
 
@end

這個(gè)代理定義了Model層的初始接口杜顺,并將搜索Flickr的責(zé)任從ViewModel中分離出來(lái)财搁。

接下來(lái)在同一個(gè)分組中添加一個(gè)NSObject的子類,名為RWTFlickrSearchImpl哑舒,并遵守剛添加的協(xié)議:

@import Foundation;
#import "RWTFlickrSearch.h"
 
@interface RWTFlickrSearchImpl : NSObject <RWTFlickrSearch>
 
@end

打開RWTFlickrSearchImpl.m并實(shí)現(xiàn)如下:

@implementation RWTFlickrSearchImpl
 
- (RACSignal *)flickrSearchSignal:(NSString *)searchString {
  return [[[[RACSignal empty]
            logAll]
            delay:2.0]
            logAll];
}
 
@end

對(duì)此你有沒有感到似曾相識(shí)妇拯?如果有,那是因?yàn)檫@是與先前在ViewModel中相同的假實(shí)現(xiàn)洗鸵。

下一步就要把Model層在ViewModel中用起來(lái)越锈。在ViewModel分組中添加名為RWTViewModelServices協(xié)議如下:

@import Foundation;
#import "RWTFlickrSearch.h"
 
@protocol RWTViewModelServices <NSObject>
 
- (id<RWTFlickrSearch>) getFlickrSearchService;
 
@end

這協(xié)議定義了一個(gè)方法,用以允許ViewModel獲取一個(gè)遵守RWTFlickrSearch協(xié)議的實(shí)例引用膘滨。

打開RWTFlickrSearchViewModel.h并引入這個(gè)新協(xié)議:

#import "RWTViewModelServices.h"

并添加以這為入?yún)⒌某跏蓟椒ǎ?/p>

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

RWTFlickrSearchViewModel.m中甘凭,添加類擴(kuò)展和指向服務(wù)引用的私有屬性:

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

在同一個(gè)文件下,更新初始化方法(譯注:原init方法)如下:

- (instancetype) initWithServices:(id<RWTViewModelServices>)services {
  self = [super init];
  if (self) {
    _services = services;
    [self initialize];
  }
  return self;
}

這簡(jiǎn)單地存儲(chǔ)了服務(wù)的引用火邓。

最后丹弱,更新executeSearchSignal方法如下:
Finally, update the executeSearchSignal method as follows.

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

以上的方法現(xiàn)在委托Model去進(jìn)行搜索工作。

最后一步就是連接Model和ViewModel铲咨。

在項(xiàng)目的項(xiàng)目根分組中添加一個(gè)命名為RWTViewModelServicesImplNSObject子類躲胳。打開RWTViewModelServicesImpl.h并遵守RWTViewModelServices協(xié)議:

@import Foundation;
#import "RWTViewModelServices.h"
 
@interface RWTViewModelServicesImpl : NSObject <RWTViewModelServices>
 
@end

打開RWTViewModelServicesImpl.m并實(shí)現(xiàn)如下:

#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

這個(gè)類簡(jiǎn)單地創(chuàng)建了一個(gè)RWTFlickrSearchImpl實(shí)例,用以搜索Flickr的Model層服務(wù)纤勒,并按需提供給ViewModel坯苹。

最后,打開RWTAppDelegate.m并添加導(dǎo)入如下:

#import "RWTViewModelServicesImpl.h"

并添加一個(gè)新的私有屬性:

@property (strong, nonatomic) RWTViewModelServicesImpl *viewModelServices;

然后更新createInitialViewController方法如下:

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

編譯運(yùn)行摇天,并確保應(yīng)用像先前一樣運(yùn)行無(wú)誤粹湃。

但這不是這些改變最激動(dòng)人心的地方,看一下這些新代碼的構(gòu)成泉坐。

Model層提供了一個(gè)ViewModel使用的“服務(wù)”为鳄。而一個(gè)協(xié)議定義了這個(gè)服務(wù)的接口,實(shí)現(xiàn)了松耦合腕让。

你可以用這個(gè)方式去為單元測(cè)試提供一個(gè)假的服務(wù)實(shí)例孤钦。應(yīng)用現(xiàn)在已經(jīng)是真正的Model-View-ViewModel結(jié)構(gòu)了。簡(jiǎn)單總結(jié)一下:

  1. Model層提供服務(wù)并負(fù)責(zé)應(yīng)用的業(yè)務(wù)邏輯纯丸。在本例中偏形,它提供了搜索Flickr的服務(wù)。
  2. ViewModel代表應(yīng)用的視圖狀態(tài)液南。它同時(shí)也響應(yīng)用戶交互和處理Model層的事件壳猜,并反映在視圖狀態(tài)的改變上。
  3. View層則非常輕量滑凉,只簡(jiǎn)單提供了ViewModel狀態(tài)的可視化和轉(zhuǎn)發(fā)用戶交互统扳。

注意:在本應(yīng)用中喘帚,Model層使用ReactiveCocoa的信號(hào)提供了服務(wù)。這個(gè)框架強(qiáng)大之處可并不僅僅在于綁定咒钟!

搜索Flickr

在這個(gè)部分吹由,你將要真正實(shí)現(xiàn)Flickr的搜索,是的朱嘴,事情變得越來(lái)越有趣了;]

第一步先創(chuàng)建代表搜索結(jié)果的Model對(duì)象倾鲫。

在Model分組下,添加一個(gè)新的NSObject子類萍嬉,名為RWTFlickrPhoto乌昔,并添加3個(gè)屬性如下:

@interface RWTFlickrPhoto : NSObject
 
@property (strong, nonatomic) NSString *title;
@property (strong, nonatomic) NSURL *url;
@property (strong, nonatomic) NSString *identifier;
 
@end

這個(gè)Model對(duì)象代表Flickr搜索接口返回的單幅照片數(shù)據(jù)。

打開RWTFlickrPhoto.m文件并添加describe方法實(shí)現(xiàn):

- (NSString *)description {
  return self.title;
}

這方便你在變更UI之前先在控制臺(tái)測(cè)試打印搜索實(shí)現(xiàn)的結(jié)果壤追。

然后磕道,添加另一個(gè)Model對(duì)象RWTFlickrSearchResults,同為NSObject的子類行冰,其中屬性如下:

@import Foundation;
 
@interface RWTFlickrSearchResults : NSObject
 
@property (strong, nonatomic) NSString *searchString;
@property (strong, nonatomic) NSArray *photos;
@property (nonatomic) NSUInteger totalResults;
 
@end

這代表著一個(gè)Flickr搜索返回的照片集合溺蕉。

打開RWTFlickrSearchResults.m并添加describe方法實(shí)現(xiàn)(同樣為了方便日志打印):

- (NSString *)description {
  return [NSString stringWithFormat:@"searchString=%@, totalresults=%lU, photos=%@",
          self.searchString, self.totalResults, self.photos];
}

接下來(lái)是時(shí)候編寫搜索Flickr的代碼了悼做!

打開RWTFlickrSearchImpl.m并添加導(dǎo)入如下:

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

這導(dǎo)入了你剛創(chuàng)建的Model對(duì)象和兩個(gè)CocoaPods加載的外部引用:

  • ObjectiveFlickr:這是基于Objective-C封裝的Flickr接口庫(kù)疯特。它負(fù)責(zé)處理相關(guān)授權(quán)和解析接口返回。相比直接使用Flickr的接口肛走,使用這個(gè)庫(kù)會(huì)更為簡(jiǎn)單漓雅。
  • LinqToObjectiveC:這個(gè)庫(kù)提供了一系列流暢的函數(shù)式接口,用于對(duì)數(shù)組和字典進(jìn)行查詢羹与,過濾和轉(zhuǎn)換故硅。

繼續(xù)在RWTFlickrSearchImpl.m中添加類擴(kuò)展如下:

@interface RWTFlickrSearchImpl () <OFFlickrAPIRequestDelegate>
 
@property (strong, nonatomic) NSMutableSet *requests;
@property (strong, nonatomic) OFFlickrAPIContext *flickrContext;
 
@end

這遵守了來(lái)自ObjectiveFlickr庫(kù)的OFFlickrAPIRequestDelegate協(xié)議庶灿,并添加了兩個(gè)私有屬性纵搁。你很快就能看到這是如何使用的。

在同一個(gè)文件下添加如下初始化方法如下:

- (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;
}

這創(chuàng)建了一個(gè)Flickr的“上下文”往踢,用以儲(chǔ)存ObjectiveFlickr生成接口請(qǐng)求的必須數(shù)據(jù)腾誉。

注意:要使用ObjectiveFlickr,你要在Flickr App Garden中創(chuàng)建一個(gè)Flickr的應(yīng)用key峻呕。那免費(fèi)而且只有簡(jiǎn)答的幾個(gè)步驟利职。
注意申請(qǐng)的是非商業(yè)用途的key。

ObjectiveFlickr接口相當(dāng)?shù)湫褪莅D銊?chuàng)建接口請(qǐng)求猪贪,結(jié)果的成功與否將通過代理方法返回,具體方法定義在先前提到的OFFlickrAPIRequestDelegate協(xié)議中讯私。

現(xiàn)有由Model層提供的热押,定義在RWTFlickrSearch協(xié)議中的接口西傀,只有一個(gè)根據(jù)搜索字符串搜索圖片的方法。

然而桶癣,接下來(lái)你還需要添加更多的方法拥褂。

正因如此,為了優(yōu)化代碼牙寞,你將直接用更加聰明的方式饺鹃,使用信號(hào)改造這些基于代理的接口。

繼續(xù)在RWTFlickrSearchImpl.m中添加方法如下:

- (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];
    }];
  }];
}

這個(gè)方法根據(jù)方法名和傳遞參數(shù)發(fā)起了一個(gè)接口請(qǐng)求间雀,并使用提供的block參數(shù)轉(zhuǎn)換接口返回悔详。你很快就能看到這是如何運(yùn)作的。

這個(gè)方法相當(dāng)長(zhǎng)惹挟,我們按步驟分析一下:

  1. createSignal方法創(chuàng)建了一個(gè)新信號(hào)伟端。傳遞給block的subscriber參數(shù)用于發(fā)送next, error 和completed事件給信號(hào)的訂閱者。
  2. 一個(gè)ObjectiveFlickr的請(qǐng)求被構(gòu)建匪煌,這個(gè)請(qǐng)求的引用被保存到requests集合中责蝠。沒有這行代碼,OFFlickrAPIRequest將不作保留萎庭!
  3. rac_signalForSelector:fromProtocol:方法根據(jù)代理方法創(chuàng)建了一個(gè)信號(hào)霜医,用于表明Flickr接口請(qǐng)求的結(jié)果。
  4. 訂閱這個(gè)信號(hào)(代理方法生成的)驳规,并轉(zhuǎn)換結(jié)果肴敛,然后作為事件值發(fā)送給第一步創(chuàng)建的信號(hào)(之后我們還會(huì)詳細(xì)談?wù)勥@個(gè))
  5. 調(diào)用ObjectiveFlickr的接口請(qǐng)求。
  6. 當(dāng)信號(hào)處理完成吗购,這個(gè)block保證了Flickr請(qǐng)求的引用會(huì)被移除医男,避免了內(nèi)存泄漏。

現(xiàn)在我們重點(diǎn)看一下第四部:

[[[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:方法創(chuàng)建了successSignal信號(hào)捻勉,它同時(shí)也在代理方法的調(diào)用中創(chuàng)建了信號(hào)镀梭。

每當(dāng)代理方法被調(diào)用,一個(gè)next事件就會(huì)發(fā)送踱启,其中附帶一個(gè)包含方法入?yún)⒌?code>RACTuple报账。而上面的管道則進(jìn)行了如下幾步處理:

  1. map方法從flickrAPIRequest:didCompleteWithResponse:代理方法中提取了它的第二個(gè)NSDictionary類型的參數(shù)。
  2. block作為參數(shù)傳遞給第二個(gè)map方法埠偿,用以轉(zhuǎn)換結(jié)果透罢。你接下來(lái)能看到這是如何把字典轉(zhuǎn)換成model對(duì)象的。
  3. 最后冠蒋,轉(zhuǎn)換的結(jié)果作為next事件被發(fā)送羽圃,信號(hào)完成結(jié)束。

注意:這段代碼有一個(gè)小小的問題抖剿,考慮一下朽寞,如果當(dāng)有多個(gè)并發(fā)請(qǐng)求時(shí)會(huì)發(fā)生什么胚吁?在這個(gè)MVVM教程的下半部分你將解決這個(gè)問題,但如果你喜歡挑戰(zhàn)自己的話愁憔,何不現(xiàn)在就嘗試解決一下呢腕扶?

最后一步就是實(shí)現(xiàn)Flickr的搜索方法如下:

- (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參數(shù)傳入的block把NSDictionary類型的返回轉(zhuǎn)換成對(duì)等的model對(duì)象,以便于在ViewModel中使用膜宋。

這段代碼使用了LinqToObjectiveC添加到NSArray中的linq_select方法窿侈,以函數(shù)式的接口進(jìn)行數(shù)組轉(zhuǎn)換。

注意:對(duì)于更加復(fù)雜的JSON轉(zhuǎn)對(duì)象操作秋茫,我推薦使用 Mantle史简。盡管在這個(gè)特殊的model中,要正確映射則需要用到它的2.0特性肛著。

最后圆兵,打開RWTFlickrSearchViewModel.m,并更新搜索信號(hào)枢贿,打印它的搜索結(jié)果殉农。

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

編譯運(yùn)行并輸入搜索字符,查看這個(gè)信號(hào)打印在控制臺(tái)的結(jié)果:

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
)

注意:如果你沒有獲取到結(jié)果局荚,那么請(qǐng)?jiān)俅螜z查你的Flickr接口秘鑰和分享密碼(Flickr API key and shared secret)超凳。

至此這個(gè)MVVM教程的上半部分即將結(jié)束,而在結(jié)束之前耀态,當(dāng)前應(yīng)用代碼中還有一個(gè)非常重要的方面你是沒有顧及到的……

內(nèi)存管理

我曾在先前的教程中提到轮傍,在你使用基于block的ReactiveCocoa接口時(shí)要小心避免引用循環(huán),那最終會(huì)導(dǎo)致內(nèi)存的泄漏首装。

如果一個(gè)block中使用了“self”创夜,而“self”又強(qiáng)引用了該block,就會(huì)造成引用循環(huán)簿盅。而在早前的教程中你看到了如何使用@weakify@strongify宏去打破這些引用循環(huán)挥下。

那你有否因?yàn)?code>signalFromAPIMethod:arguments:transform:在引用self時(shí)沒有使用這些宏而感到疑惑揍魂?

那是因?yàn)槟莻€(gè)block是作為參數(shù)傳入到createSignal:方法中的桨醋,那并不會(huì)在self和block之間構(gòu)成強(qiáng)引用。

很混亂现斋?好吧喜最,你不一定要相信我的話,就直接測(cè)試這代碼去看看有沒內(nèi)存泄漏好了庄蹋。

使用Product / Profile選項(xiàng)運(yùn)行引用瞬内,選中Allocations配置迷雪。當(dāng)應(yīng)用開始運(yùn)行,使用右上角的過濾器搜索包含“OFF”的類虫蝶,比如ObjectiveFlickr框架中的類章咧。(譯注:在最新的Xcode 8.3.1版本,該過濾輸入框在左下角能真。)

你會(huì)發(fā)現(xiàn)在應(yīng)用啟動(dòng)時(shí)創(chuàng)建了一個(gè)單例OFFlickrAPIContext赁严。當(dāng)開始搜索時(shí),一個(gè)OFFlickrAPIRequest的實(shí)例被創(chuàng)建并在搜索期間一直存在:


好消息是粉铐,當(dāng)接口請(qǐng)求結(jié)束疼约,OFFlickrAPIRequest實(shí)例就會(huì)被回收。這證明了發(fā)起Flickr請(qǐng)求的block并沒有被持有蝙泼!

我建議你在應(yīng)用開發(fā)中周期性地重復(fù)這項(xiàng)分析操作程剥,以保證內(nèi)存堆中只有你期望的對(duì)象。

何去何從?

這個(gè)示例項(xiàng)目包含了這個(gè)教程至今的所有代碼汤踏。這總結(jié)了這個(gè)MVVM上半部教程的所有內(nèi)容织鲸!

在教程下一部分,你將關(guān)注與如何從ViewModel轉(zhuǎn)換視圖控制器溪胶,和實(shí)現(xiàn)更多的Flickr接口去豐富應(yīng)用的功能昙沦。

譯注:下半部原文不知何種原因錯(cuò)漏較多,故而不發(fā)布翻譯载荔。在第二部分中盾饮,主要涉及的是通過ViewModel層進(jìn)行視圖控制器的跳轉(zhuǎn)。關(guān)于這個(gè)話題懒熙,建議閱讀MVVM With ReactiveCocoa和研究其例子中的源碼丘损。里面的實(shí)現(xiàn)更為全面。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末工扎,一起剝皮案震驚了整個(gè)濱河市徘钥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肢娘,老刑警劉巖呈础,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異橱健,居然都是意外死亡而钞,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門拘荡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)臼节,“玉大人,你說(shuō)我怎么就攤上這事⊥欤” “怎么了巨税?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)粉臊。 經(jīng)常有香客問我草添,道長(zhǎng),這世上最難降的妖魔是什么扼仲? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任果元,我火速辦了婚禮,結(jié)果婚禮上犀盟,老公的妹妹穿的比我還像新娘而晒。我一直安慰自己,他們只是感情好阅畴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布倡怎。 她就那樣靜靜地躺著,像睡著了一般贱枣。 火紅的嫁衣襯著肌膚如雪监署。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天纽哥,我揣著相機(jī)與錄音钠乏,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的浊吏。 我是一名探鬼主播辛萍,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼步淹,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤锅必,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后惕艳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搞隐,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年远搪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了劣纲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡终娃,死狀恐怖味廊,靈堂內(nèi)的尸體忽然破棺而出蒸甜,到底是詐尸還是另有隱情棠耕,我是刑警寧澤余佛,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站窍荧,受9級(jí)特大地震影響辉巡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蕊退,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一郊楣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瓤荔,春花似錦净蚤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至点把,卻和暖如春橘荠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背郎逃。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工哥童, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人褒翰。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓贮懈,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親优训。 傳聞我的和親對(duì)象是個(gè)殘疾皇子错邦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容