1 MVVM簡(jiǎn)介
1.1 MVC介紹
????????MVC: Massive View Controller. Alot of the time, it’s convenient to put business logic and other code into viewcontrollers, even if that’s not architecturally the most sound place to put it.
????????Use of MVVM helps reduce theamount of business logic in view controllers, which helps reduce their bloatedsize and also makes that business logic more testable.
1.2 MVVM要點(diǎn)
????????In MVVM, we tend to consider the view andthe view controller as one entity (which explains why it’s not called MVVCVM).Instead of the view controller performing a lot of those mediations betweenmodel and view, the view model does, instead.
????????ReactiveCocoa will monitor for changes inthe model and map those changes to properties on the view model, performing anynecessary business logic.
1.3 MVVM示例
????????As a concrete example, imagine our modelcontains a date called dateAdded that we want to monitor for changes, andupdate our view model’s dateAdded property. The model’s property is an NSDate instance,while the view model’s is a transformed NSString. The binding would looksomething like the following (inside the view model’s init method).
RAC(self,dateAdded) = [RACObserve(self.model, dateAdded) map:^(NSDate *date) {
????return [[ViewModel dateFormatter] stringFromDate: date];
}];
????????dateFormatter is a class method onViewModel that caches an NSDateFormatter instance so it can be reused (they’re expensive to create). Then, the view controller can monitor the view model’s
dateAdded property, binding it to a label.
RAC(self.label, text) = RACObserve(self.viewModel, dateAdded);
????????We’ve now abstracted the logic of transforming the date to a string into our view model, where we might write unit tests for it. It seems contrived in this trivial example, but as we’ll see,it helps reduce the amount of logic in your view controllers significantly.
????????此處有三個(gè)重點(diǎn)是我希望你看完本文能帶走的:
? MVVM可以兼容你當(dāng)下使用的MVC架構(gòu)。
? MVVM增加你的應(yīng)用的可測(cè)試性凳谦。
? MVVM配合一個(gè)綁定機(jī)制效果最好怀吻。
????????如我們之前所見(jiàn),MVVM基本上就是MVC的改進(jìn)版箕速,所以很容易就能看到它如何被整合到現(xiàn)有使用典型MVC架構(gòu)的應(yīng)用中零截。讓我們看一個(gè)簡(jiǎn)單的PersonModel以及相應(yīng)的View Controller:
@interface Person : NSObject
?@property (nonatomic, readonly) NSString*salutation;
@property (nonatomic, readonly) NSString*firstName;
@property (nonatomic, readonly) NSString*lastName;
@property (nonatomic, readonly) NSDate*birthdate;
?-?(instancetype) initwithSalutation: (NSString?*)salutation firstName:(NSString*)firstName lastName:(NSString?*)lastName birthdate:(NSDate*)birthdate;
@end
????????Cool锭汛!現(xiàn)在我們假設(shè)我們有一個(gè) PersonViewController,在 viewDidLoad里管呵,只需要基于它的 model屬性設(shè)置一些Label即可梳毙。
- (void)viewDidLoad{
??? [superviewDidLoad];
??? if (self.model.salutation.length > 0) {
??????? self.nameLabel.text = [NSStringstringWithFormat:@"%@
%@ %@", self.model.salutation, self.model.firstName, self.model.lastName];
??? }else{
??????? self.nameLabel.text = [NSStringstringWithFormat:@"%@
%@", self.model.firstName, self.model.lastName];
??? }
??? NSDateFormatter *dateFormatter =[[NSDateFormatter alloc] init];
??? [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
??? self.birthdateLabel.text = [dateFormatter stringFromDate: model.birthdate];
}
????這全都直截了當(dāng),標(biāo)準(zhǔn)的MVC【柘拢現(xiàn)在來(lái)看看我們?nèi)绾斡靡粋€(gè)View Model來(lái)增強(qiáng)它账锹。
@interface PersonViewModel : NSObject
@property (nonatomic, readonly) Person *person;
@property (nonatomic, readonly) NSString*nameText;
@property (nonatomic, readonly) NSString*birthdateText;
-(instancetype) initWithPerson: (Person *)person;
@end
????我們的View Model的實(shí)現(xiàn)大概如下:
@implementation PersonViewModel
- (instancetype) initWithPerson: (Person *)person {
??? self = [super init];
??? if (!self) return nil;
??? _person = person;
??? if (person.salutation.length > 0) {
??????? _nameText = [NSString stringWithFormat: @"%@
%@ %@", self.person.salutation, self.person.firstName, self.person.lastName];
??? }else{
??????? _nameText = [NSString stringWithFormat: @"%@ %@", self.person.firstName, self.person.lastName];
??? }
??? NSDateFormatter *dateFormatter =[[NSDateFormatter alloc] init];
??? [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"];
??? _birthdateText = [dateFormatter stringFromDate: person.birthdate];
??? return self;
}
@end
Cool萌业!我們已經(jīng)將 viewDidLoad中的表示邏輯放入我們的View Model里了。此時(shí)奸柬,我們新的 viewDidLoad就會(huì)非常輕量:
- (void)viewDidLoad{
??? [superviewDidLoad];
??? self.nameLabel.text = self.viewModel.nameText;
??? self.birthdateLabel.text = self.viewModel.birthdateText;
}
????????所以生年,如你所見(jiàn),并沒(méi)有對(duì)我們的MVC架構(gòu)做太多改變廓奕。還是同樣的代碼抱婉,只不過(guò)移動(dòng)了位置。它與MVC兼容桌粉,帶來(lái)更輕量的 View Controllers蒸绩。
????????可測(cè)試,嗯铃肯?是怎樣患亿?好吧,ViewController是出了名的難以測(cè)試押逼,因?yàn)樗鼈冏隽颂嗍虑椴脚骸T贛VVM里,我們?cè)囍M可能多的將代碼移入View Model里挑格。測(cè)試ViewController就變得容易多了咙冗,因?yàn)樗鼈儾辉僮鲆淮蠖咽虑椋⑶襐iew Model也非常易于測(cè)試恕齐。讓我們來(lái)看看:
SpecBegin(Person)
??? NSString *salutation = @"Dr.";
??? NSString *firstName = @"first";
??? NSString *lastName = @"last";
??? NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970: 0];
??? it (@"should use the salutation available. ", ^{
??????? Person *person = [[Person alloc] initWithSalutation: salutation firstName: firstName lastName: lastName birthdate: birthdate];
??????? PersonViewModel *viewModel =[[PersonViewModel alloc] initWithPerson:person];
??????? expect(viewModel.nameText).to.equal(@"Dr. first last");
??? });
??? it (@"should not use an unavailable salutation. ", ^{
??????? Person *person = [[Person alloc] initWithSalutation: nil firstName: firstName lastName: lastName birthdate: birthdate];
??????? PersonViewModel *viewModel =[[PersonViewModel alloc] initWithPerson: person];
??????? expect(viewModel.nameText).to.equal(@"first last");
??? });
??? it (@"should use the correct date format. ", ^{
??????? Person *person = [[Person alloc] initWithSalutation: nil firstName: firstName lastName: lastName birthdate: birthdate];
??????? PersonViewModel *viewModel =[[PersonViewModel alloc] initWithPerson: person];
??????? expect(viewModel.birthdateText).to.equal(@"Thursday January 1, 1970");
??? });
SpecEnd
????????注意到在這個(gè)簡(jiǎn)單的例子中乞娄,Model是不可變的瞬逊,所以我們可以只在初始化的時(shí)候指定我們View Model的屬性显歧。對(duì)于可變Model,我們還需要使用一些綁定機(jī)制确镊,這樣View Model就能在背后的Model改變時(shí)更新自身的屬性士骤。此外,一旦View Model上的Model發(fā)生改變蕾域,那View的屬性也需要更新拷肌。Model的改變應(yīng)該級(jí)聯(lián)向下通過(guò)ViewModel進(jìn)入View。
????????在OS X上旨巷,我們可以使用Cocoa綁定巨缘,但在iOS上我們并沒(méi)有這樣好的配置可用。我們想到了KVO(Key-Value Observation)采呐,而且它確實(shí)做了很偉大的工作若锁。然而,對(duì)于一個(gè)簡(jiǎn)單的綁定都需要很大的樣板代碼斧吐,更不用說(shuō)有許多屬性需要綁定了又固。作為替代仲器,我個(gè)人喜歡使用ReactiveCocoa,但MVVM并未強(qiáng)制我們使用ReactiveCocoa仰冠。MVVM是一個(gè)偉大的典范乏冀,它自身獨(dú)立,只是在有一個(gè)良好的綁定框架時(shí)做得更好洋只。
2 ReactiveCocoa
ReactiveCocoa指南二:Twitter搜索實(shí)例
2.1 響應(yīng)式編程FRP
2.1.1 函數(shù)式編程 (FunctionalProgramming)
??? ????函數(shù)式編程也可以寫(xiě)N篇,它是完全不同于OO的編程模式辆沦,這?里主要講一下這個(gè)框架使?用到的函數(shù)式思想。
????????(1) 高階函數(shù):在函數(shù)式編程中识虚,把函數(shù)當(dāng)參數(shù)來(lái)回傳遞众辨,而這個(gè),說(shuō)成術(shù)語(yǔ)舷礼,我們把他叫做高階函數(shù)鹃彻。在oc中,blocks是被廣泛使?用的參數(shù)傳遞妻献,它實(shí)際上是匿名函數(shù)蛛株。
????????高階函數(shù)調(diào)用過(guò)程有點(diǎn)像linux命令?里的pipeline(管道),一個(gè)命令調(diào)用后的輸出當(dāng)作另一個(gè)命令輸入育拨,多個(gè)命令之間可以串起來(lái)操作谨履。來(lái)個(gè)例子:
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
// Contains: 22 44 66 88
RACSequence *doubleNumber = [[numbers filter:^ BOOL(NSString *value) {
????return (value.intValue % 2) == 0;
}] map:^id(id value) {
????return [value stringByAppendingString: value];
}];
????????上面的例子是數(shù)組里的值先進(jìn)行過(guò)濾,得到偶數(shù)熬丧,然后再將每個(gè)值進(jìn)行stringByAppendingString笋粟,最終輸出22 44 66 88.
????????(2) 惰性(或延遲)求值:Sequences對(duì)象等,只有當(dāng)被使用到時(shí)析蝴,才會(huì)對(duì)其求值害捕。
????????關(guān)于函數(shù)編程,有興趣的大家可以研究下haskell或者clojure闷畸,不過(guò)目前好多語(yǔ)?言都在借用函數(shù)式的思想尝盼。
2.1.2 響應(yīng)式編程(Functional Reactive Programming:FRP)
????????響應(yīng)式編程是一種和事件流有關(guān)的編程模式,關(guān)注導(dǎo)致?tīng)顟B(tài)值改變的行為事件佑菩,一系列事件組成了事件流盾沫。一系列事件是導(dǎo)致屬性值發(fā)生變化的原因。FRP非常類(lèi)似于設(shè)計(jì)模式里的觀察者模式殿漠。
????????響應(yīng)式編程是一種針對(duì)數(shù)據(jù)流和變化傳遞的編程模式赴精,其執(zhí)行引擎可以自動(dòng)的在數(shù)據(jù)流之間傳遞數(shù)據(jù)的變化。比如說(shuō)绞幌,在一種命令式編程語(yǔ)言中蕾哟,a: = b + c 表示 a 是 b + c 表達(dá)式的值,但是在RP語(yǔ)言中,它可能意味著一個(gè)動(dòng)態(tài)的數(shù)據(jù)流關(guān)系:當(dāng)c或者b的值發(fā)生變化時(shí)渐苏,a的值自動(dòng)的發(fā)生變化掀潮。
????????RP已經(jīng)被證實(shí)是一種最有效的處理交互式用戶界面、實(shí)時(shí)模式下的動(dòng)畫(huà)的開(kāi)發(fā)模式琼富,但本質(zhì)上是一種基本的編程模式∫前桑現(xiàn)在最為熱門(mén)的JavaFX腳本語(yǔ)言中,引入的bind就是RP的一個(gè)概念實(shí)現(xiàn)鞠眉。
響應(yīng)式編程其關(guān)鍵點(diǎn)包括:
????(1) 輸入被視為"行為"薯鼠,或者說(shuō)一個(gè)隨時(shí)間而變化的事件流
????(2) 連續(xù)的、隨時(shí)間而變化的值
????(3) 按時(shí)間排序的離散事件序列
????????FRP與普通的函數(shù)式編程相似械蹋,但是每個(gè)函數(shù)可以接收一個(gè)輸入值的流出皇,如果其中,一個(gè)新的輸入值到達(dá)的話哗戈,這個(gè)函數(shù)將根據(jù)最新的輸入值重新計(jì)算郊艘,并且產(chǎn)?生一個(gè)新的輸出。這是一種”數(shù)據(jù)流"編程模式唯咬。
2.2 ReactiveCocoa試圖解決什么問(wèn)題
????????經(jīng)過(guò)一段時(shí)間的研究纱注,我認(rèn)為ReactiveCocoa試圖解決以下3個(gè)問(wèn)題:
? ? 1、傳統(tǒng)iOS開(kāi)發(fā)過(guò)程中胆胰,狀態(tài)以及狀態(tài)之間依賴過(guò)多的問(wèn)題狞贱;
? ? 2、傳統(tǒng)MVC架構(gòu)的問(wèn)題:Controller比較復(fù)雜蜀涨,可測(cè)試性差瞎嬉;
? ? 3、提供統(tǒng)一的消息傳遞機(jī)制厚柳;
????????(1) 開(kāi)發(fā)過(guò)程中氧枣,狀態(tài)以及狀態(tài)之間依賴過(guò)多,RAC更加有效率地處理事件流,而無(wú)需顯式去管理狀態(tài)草娜。在OO或者過(guò)程式編程中挑胸,狀態(tài)變化是最難跟蹤痒筒,最頭痛的事宰闰。這個(gè)也是最重要的一點(diǎn)。
????????(2) 減少變量的使用簿透,由于它跟蹤狀態(tài)和值的變化移袍,因此不需要再申明變量不斷地觀察狀態(tài)和更新值。
????????(3) 提供統(tǒng)一的消息傳遞機(jī)制老充,將oc中的通知葡盗,action,KVO以及其它所有UIControl事件的變化都進(jìn)行監(jiān)控袜匿,當(dāng)變化發(fā)生時(shí)匆骗,就會(huì)傳遞事件和值。
????????(4) 當(dāng)值隨著事件變換時(shí)垂蜗,可以使用map喘先,filter钳吟,reduce等函數(shù)便利地對(duì)值進(jìn)行變換操作。
2.3 試圖解決MVC框架的問(wèn)題
????????我們?cè)陂_(kāi)發(fā)iOS應(yīng)用時(shí)窘拯,一個(gè)界面元素的狀態(tài)很可能受多個(gè)其它界面元素或后臺(tái)狀態(tài)的影響红且。
????????RAC的信號(hào)機(jī)制很容易將某一個(gè)Model變量的變化與界面關(guān)聯(lián),所以非常容易應(yīng)用Model-View-ViewModel框架涤姊。通過(guò)引入ViewModel層暇番,然后用RAC將ViewModel與View關(guān)聯(lián),View層的變化可以直接響應(yīng)ViewModel層的變化思喊,這使得Controller變得更加簡(jiǎn)單壁酬,由于View不再與Model綁定,也增加了View的可重用性恨课。
????????因?yàn)橐肓薞iewModel層厨喂,所以單元測(cè)試可以在ViewModel層進(jìn)行,iOS工程的可測(cè)試性也大大增強(qiáng)了庄呈。InfoQ也曾撰文介紹過(guò)MVVM:《MVVM啟示錄》 蜕煌。
2.4 統(tǒng)一消息傳遞機(jī)制
????????iOS開(kāi)發(fā)中有著各種消息傳遞機(jī)制,包括KVO诬留、Notification斜纪、delegation、block以及target-action方式文兑。各種消息傳遞機(jī)制使得開(kāi)發(fā)者在做具體選擇時(shí)感到困惑盒刚,例如在objc.io上就有專門(mén)撰文(破船的翻譯 ),介紹各種消息傳遞機(jī)制之間的差異性绿贞。
????????RAC將傳統(tǒng)的UI控件事件進(jìn)行了封裝因块,使得以上各種消息傳遞機(jī)制都可以用RAC來(lái)完成。
2.5 使用時(shí)機(jī)
2.5.1 (1)處理異步或者事件驅(qū)動(dòng)的數(shù)據(jù)變化
static void *ObservationContext = &ObservationContext;
- (void)viewDidLoad {
????[super viewDidLoad];
????[LoginManager.sharedManager addObserver: self forKeyPath: @"loggingIn" options: NSKeyValueObservingOptionInitial context: &ObservationContext];
????[NSNotificationCenter.defaultCenter addObserver: self selector: @selector(loggedOut:) name: UserDidLogOutNotification object: LoginManager.sharedManager];
????[self.usernameTextField addTarget: self action: @selector(updateLogInButton) forControlEvents: UIControlEventEditingChanged];
????[self.passwordTextField addTarget: self action: @selector(updateLogInButton) forControlEvents: UIControlEventEditingChanged];
????[self.logInButton addTarget: self action: @selector(logInPressed:) forControlEvents: UIControlEventTouchUpInside];
}
- (void)dealloc {
????[LoginManager.sharedManager removeObserver: self forKeyPath: @"loggingIn" context: ObservationContext];
????[NSNotificationCenter.defaultCenter removeObserver: self];
}
- (void)updateLogInButton {
????BOOL textFieldsNonEmpty =self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0;
????BOOL readyToLogIn =!LoginManager.sharedManager.isLoggingIn && !self.loggedIn;
????self.logInButton.enabled = textFieldsNonEmpty &&readyToLogIn;
}
- (IBAction)logInPressed:(UIButton *)sender {
????[[LoginManager sharedManager] logInWithUsername: self.usernameTextField.text password: self.passwordTextField.text success:^{
????self.loggedIn = YES;
????} failure:^(NSError *error) {
????????[self presentError: error];
????}];
}
- (void)loggedOut:(NSNotification *)notification {
????self.loggedIn = NO;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change: (NSDictionary *)change context:(void *)context {
????if (context == ObservationContext) {
????????[self updateLogInButton];
? ? } else {
????????[super observeValueForKeyPath: keyPath ofObject: object change: change context: context];
????}
}
// RAC實(shí)現(xiàn):
- (void)viewDidLoad {
????[super viewDidLoad];
????@weakify(self);
????RAC(self.logInButton, enabled) = [RACSignal combineLatest: @[self.usernameTextField.rac_textSignal, self.passwordTextField.rac_textSignal, RACObserve(LoginManager.sharedManager, loggingIn), RACObserve(self, loggedIn)
] reduce:^(NSString *username, NSString *password,NSNumber *loggingIn, NSNumber *loggedIn) {
????return @(username.length > 0 &&password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);
}];
????[[self.logInButton rac_signalForControlEvents: UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) {
????@strongify(self);
????RACSignal *loginSignal = [LoginManager.sharedManager logInWithUsername: self.usernameTextField.text password: self.passwordTextField.text];
????[loginSignal subscribeError:^(NSError *error) {
????????@strongify(self);
????????[self presentError: error];
? ? } completed:^{
????????@strongify(self);
????????self.loggedIn = YES;
????}];
}];
RAC(self, loggedIn) =[[NSNotificationCenter.defaultCenter rac_addObserverForName: UserDidLogOutNotificationobject: nil] mapReplace: @NO];
}
2.5.2 (2)鏈?zhǔn)降囊蕾嚥僮?/h3>
[client logInWithSuccess:^{
????[client loadCachedMessagesWithSuccess:^(NSArray*messages) {
????????[client fetchMessagesAfterMessage: messages.lastObject success:^(NSArray *nextMessages) {
????????????NSLog(@"Fetched all messages.");
????????} failure:^(NSError *error) {
????????????[self presentError: error];
????????}];
????} failure:^(NSError *error) {
????????[self presentError: error];
????}];
} failure:^(NSError *error) {
????[self presentError: error];
}];
// RAC實(shí)現(xiàn):
[[[[client logIn] then:^{
????return [client loadCachedMessages];
}] flattenMap:^(NSArray *messages) {
????return [client fetchMessagesAfterMessage: messages.lastObject];
}] subscribeError:^(NSError *error) {
????[self presentError: error];
} completed:^{
????NSLog(@"Fetched all messages.");
}];
2.5.3 (3)并?行依賴操作:
__block NSArray *databaseObjects;
__block NSArray *fileContents;
NSOperationQueue *backgroundQueue = [[NSOperationQueuealloc] init];
NSBlockOperation *databaseOperation = [NSBlockOperationblockOperationWithBlock:^{
????databaseObjects = [databaseClient fetchObjectsMatchingPredicate: predicate];
}];
NSBlockOperation *filesOperation = [NSBlockOperationblockOperationWithBlock:^{
????NSMutableArray *filesInProgress = [NSMutableArray array];
????for (NSString *path in files) {
????????[filesInProgress addObject:[NSData dataWithContentsOfFile: path]];
????}
????fileContents = [filesInProgress copy];
}];
NSBlockOperation *finishOperation = [NSBlockOperationblockOperationWithBlock:^{
????[self finishProcessingDatabaseObjects: databaseObjects fileContents: fileContents];
????NSLog(@"Done processing");
}];
[finishOperation addDependency: databaseOperation];
[finishOperation addDependency: filesOperation];
[backgroundQueue addOperation: databaseOperation];
[backgroundQueue addOperation: filesOperation];
[backgroundQueue addOperation: finishOperation];
//RAC
RACSignal *databaseSignal = [[databaseClient fetchObjectsMatchingPredicate: predicate] subscribeOn: [RACScheduler scheduler]];
RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler: [RACScheduler scheduler] block:^(id subscriber) {
????NSMutableArray *filesInProgress = [NSMutableArray array];
????for (NSString *path in files) {
????????[filesInProgress addObject: [NSData dataWithContentsOfFile: path]];
????}
????[subscriber sendNext: [filesInProgress copy]];
????[subscriber sendCompleted];
}];
[[RACSignal combineLatest: @[ databaseSignal, fileSignal ] reduce:^ id (NSArray *databaseObjects, NSArray*fileContents) {
????[self finishProcessingDatabaseObjects: databaseObjects fileContents: fileContents];
????return nil;
}] subscribeCompleted:^{
????NSLog(@"Done processing");
}];
2.5.4 (4)簡(jiǎn)化集合操作
NSMutableArray *results = [NSMutableArray array];
for (NSString *str in strings) {
????if (str.length < 2) {
????????continue;
????}
????NSString *newString = [str stringByAppendingString: @"foobar"];
????[results addObject: newString];
}
RAC實(shí)現(xiàn):
RACSequence *results = [[strings.rac_sequence filter:^ BOOL (NSString *str) {
????return str.length >= 2;
}] map:^(NSString *str) {
????return [str stringByAppendingString: @"foobar"];
}];
2.6 ReactiveCocoa的特點(diǎn)
????????RAC在應(yīng)用中大量使用了block籍铁,由于Objective-C語(yǔ)言的內(nèi)存管理是基于引用計(jì)數(shù)的涡上,為了避免循環(huán)引用問(wèn)題,在block中如果要引用self拒名,需要使用@weakify(self)和@strongify(self)來(lái)避免強(qiáng)引用吩愧。另外,在使用時(shí)應(yīng)該注意block的嵌套層數(shù)增显,不恰當(dāng)?shù)臑E用多層嵌套block可能給程序的可維護(hù)性帶來(lái)災(zāi)難雁佳。
????????RAC的編程方式和傳統(tǒng)的MVC方式差異巨大,所以需要較長(zhǎng)的學(xué)習(xí)時(shí)間。并且糖权,業(yè)界內(nèi)對(duì)于RAC并沒(méi)有廣泛應(yīng)用堵腹,這造成可供參考的項(xiàng)目和教程比較欠缺。 另外星澳,RAC項(xiàng)目本身也還在快速演進(jìn)當(dāng)中秸滴,1.x版本和2.x版本API改動(dòng)了許多,3.0版本也正在快速開(kāi)發(fā)中募判,對(duì)它的使用也需要考慮后期的升級(jí)維護(hù)問(wèn)題荡含。
????????作為一個(gè)iOS開(kāi)發(fā)領(lǐng)域的新開(kāi)源框架,ReactiveCocoa帶來(lái)了函數(shù)式編程和響應(yīng)式編程的思想届垫,值得大家關(guān)注并且學(xué)習(xí)释液。