本文翻译自GitHub上的开源框架ReactiveCocoa的readme,
英文原文链接https://github.com/ReactiveCocoa/ReactiveCocoa.
ReactiveCocoa (RAC)是一个Objective-C的框架,它的灵感来自函数式响应式编程.
如果你已经很熟悉函数式响应式编程编程或者了解ReactiveCocoa的一些基本前提,check outDocumentation文件夹作为框架的概述,这里面有一些关于它怎么工作的深层次的信息.
感谢Rheinfabrik对ReactiveCocoa 3!_开发慷慨地赞助.
什么是ReactiveCocoa?
ReactiveCocoa文档写得很厉害,并且详细地介绍了RAC是什么以及它是怎么工作的?
如果你多学一点,我们推荐下面这些资源:
-
PrevIoUsly answered StackOverflow questionsandGitHub issues
-
The rest of theDocumentationfolder
如果你有任何其他的问题,请随意提交issue,
介绍
ReactiveCocoa的灵感来自函数式响应式编程.Rather than using mutable variables which are replaced and modified in-place,RAC提供signals(表现为RACSignal)来捕捉当前以及将来的值.
通过对signals进行连接,绑定和响应,不需要连续地观察和更新值,软件就能写了.
举个例子,一个text field能够绑定到最新状态,即使它在变,而不需要用额外的代码去更新text field每一秒的状态.它有点像KVO,但它用blocks代替了重写-observeValueForKeyPath:ofObject:change:context:.
Signals也能够呈现异步的操作,有点像futures andpromises.这极大地简化了异步软件,包括了网络处理的代码.
RAC有一个主要的优点,就是提供了一个单一的,统一的方法去处理异步的行为,包括delegate方法,blocks回调,target-action机制,notifications和KVO.
这里有一个简单的例子:
1
2
3
4
5
6
7
8
|
//Whenself.usernamechanges,logsthenewnametotheconsole.
//
//RACObserve(self,username)createsanewRACSignalthatsendsthecurrent
//valueofself.username,thenthenewvaluewheneveritchanges.
//-subscribeNext:willexecutetheblockwheneverthesignalsendsavalue.
[RACObserve(self,username)subscribeNext:^(NSString*newName){
NSLog(@
"%@"
,newName);
}];
|
这不像KVO notifications,signals能够连接在一起并且能够同时进行操作:
//Onlylogsnamesthatstartswith"j".
//-filterreturnsanewRACSignalthatonlysendsanewvaluewhenitsblock
//returnsYES.
[[RACObserve(self,username)
filter:^(NSString*newName){
return
[newNamehasPrefix:@
"j"
];
}]
subscribeNext:^(NSString*newName){
|
Signals也能够用来导出状态.而不是observing properties或者设置其他的 properties去反应新的值,RAC通过signals and operations让表示属性变得有可能:
//Createsaone-waybindingsothatself.createEnabledwillbe
//truewheneverself.passwordandself.passwordConfirmation
//areequal.
//
//RAC()isamacrothatmakesthebindinglooknicer.
//
//+combineLatest:reduce:takesanarrayofsignals,executestheblockwiththe
//latestvaluefromeachsignalwheneveranyofthemchanges,andreturnsanew
//RACSignalthatsendsthereturnvalueofthatblockasvalues.
RAC(self,createEnabled)=[RACSignal
combineLatest:@[RACObserve(self,password),RACObserve(self,passwordConfirmation)]
reduce:^(NSString*password,NSString*passwordConfirm){
@([passwordConfirmisEqualToString:password]);
}];
|
Signals不仅仅能够用在KVO,还可以用在很多的地方.比如说,它们也能够展示button presses:
//Logsamessagewheneverthebuttonispressed.
//RACCommandcreatessignalstorepresentUIactions.Eachsignalcan
//representabuttonpress,forexample,andhaveadditionalworkassociated
//withit.
//-rac_commandisanadditiontoNSButton.Thebuttonwillsenditselfonthat
//commandwheneverit'spressed.
self.button.rac_command=[[RACCommandalloc]initWithSignalBlock:^(id_){
"buttonwaspressed!"
);
[RACSignalempty];
}];
|
或者异步的网络操作:
//Hooksupa"Login"buttontologinoverthenetwork.
//Thisblockwillberunwheneverthelogincommandisexecuted,starting
//theloginprocess.
self.loginCommand=[[RACCommandalloc]initWithSignalBlock:^(idsender){
//Thehypothetical-logInmethodreturnsasignalthatsendsavaluewhen
//thenetworkrequestfinishes.
[clientlogIn];
}];
//-executionSignalsreturnsasignalthatincludesthesignalsreturnedfrom
//theaboveblock,oneforeachtimethecommandisexecuted.
[self.loginCommand.executionSignalssubscribeNext:^(RACSignal*loginSignal){
//Logamessagewheneverweloginsuccessfully.
[loginSignalsubscribeCompleted:^{
"Loggedinsuccessfully!"
);
}];
}];
//Executesthelogincommandwhenthebuttonispressed.
self.loginButton.rac_command=self.loginCommand;
|
Signals能够展示timers,其他的UI事件,或者其他跟时间改变有关的东西.
对于用signals来进行异步操作,通过连接和改变这些signals能够进行更加复杂的行为.在一组操作完成时,工作能够很简单触发:
//Performs2networkoperationsandlogsamessagetotheconsolewhentheyare
//bothcompleted.
//
//+merge:takesanarrayofsignalsandreturnsanewRACSignalthatpasses
//throughthevaluesofallofthesignalsandcompleteswhenallofthe
//signalscomplete.
//
//-subscribeCompleted:willexecutetheblockwhenthesignalcompletes.
[[RACSignal
merge:@[[clientfetchUserRepos],[clientfetchOrgRepos]]]
subscribeCompleted:^{
"They'rebothdone!"
);
}];
|
Signals能够顺序地执行异步操作,而不是嵌套block回调.这个和futures and promises很相似:
//Logsintheuser,thenloadsanycachedmessages,thenfetchestheremaining
//messagesfromtheserver.Afterthat'salldone,logsamessagetothe
//console.
//Thehypothetical-logInUsermethodsreturnsasignalthatcompletesafter
//loggingin.
//-flattenMap:willexecuteitsblockwheneverthesignalsendsavalue,and
//returnsanewRACSignalthatmergesallofthesignalsreturnedfromtheblock
//intoasinglesignal.
[[[[client
logInUser]
flattenMap:^(User*user){
//Returnasignalthatloadscachedmessagesfortheuser.
[clientloadCachedMessagesForUser:user];
}]
flattenMap:^(NSArray*messages){
//Returnasignalthatfetchesanyremainingmessages.
[clientfetchMessagesAfterMessage:messages.lastObject];
}]
subscribeNext:^(NSArray*newMessages){
"Newmessages:%@"
"Fetchedallmessages."
);
}];
|
RAC也能够简单地绑定异步操作的结果:
//Createsaone-waybindingsothatself.imageView.imagewillbesetastheuser's
//avatarassoonasit'sdownloaded.
//Thehypothetical-fetchUserWithUsername:methodreturnsasignalwhichsends
//theuser.
//-deliverOn:createsnewsignalsthatwilldotheirworkonotherqueues.In
//thisexample,it'susedtomoveworktoabackgroundqueueandthenbacktothemainthread.
//
//-map:callsitsblockwitheachuserthat'sfetchedandreturnsanew
//RACSignalthatsendsvaluesreturnedfromtheblock.
RAC(self.imageView,image)=[[[[client
fetchUserWithUsername:@
"joshaber"
]
deliverOn:[RACSchedulerscheduler]]
map:^(User*user){
//Downloadtheavatar(thisisdoneonabackgroundqueue).
[[NSImagealloc]initWithContentsOfURL:user.avatarURL];
}]
//Nowtheassignmentwillbedoneonthemainthread.
deliverOn:RACScheduler.mainThreadScheduler];
|
这里仅仅说了RAC能做什么,但很难说清RAC为什么如此强大.虽然通过这个README很难说清RAC,但我尽可能用更少的代码,更少的模版,把更好的代码去表达清楚.
如果想要更多的示例代码,可以check outC-41或者GroceryList,这些都是真正用ReactiveCocoa写的iOS apps.更多的RAC信息可以看一下Documentation文件夹.
什么时候用ReactiveCocoa
乍看上去,ReactiveCocoa是很抽象的,它可能很难理解如何将它应用到具体的问题.
这里有一些RAC常用的地方.
处理异步或者事件驱动数据源
很多Cocoa编程集中在响应user events或者改变application state.这样写代码很快地会变得很复杂,就像一个意大利面,需要处理大量的回调和状态变量的问题.
这个模式表面上看起来不同,像UI回调,网络响应,和KVO notifications,实际上有很多的共同之处。RACSignal统一了这些API,这样他们能够组装在一起然后用相同的方式操作.
举例看一下下面的代码:
staticvoid*ObservationContext=&ObservationContext;
-(void)viewDidLoad{
[
super
viewDidLoad];
[LoginManager.sharedManageraddObserver:selfforKeyPath:@
"loggingIn"
options:NSKeyValueObservingOptionInitialcontext:&ObservationContext];
[NSNotificationCenter.defaultCenteraddObserver:selfselector:@selector(loggedOut:)name:UserDidlogoutNotificationobject:LoginManager.sharedManager];
[self.usernameTextFieldaddTarget:selfaction:@selector(updateLogInButton)forControlEvents:UIControlEventEditingChanged];
[self.passwordTextFieldaddTarget:selfaction:@selector(updateLogInButton)forControlEvents:UIControlEventEditingChanged];
[self.logInButtonaddTarget:selfaction:@selector(logInPressed:)forControlEvents:UIControlEventTouchUpInside];
}
-(void)dealloc{
[LoginManager.sharedManagerremoveObserver:selfforKeyPath:@
context:ObservationContext];
[NSNotificationCenter.defaultCenterremoveObserver:self];
}
-(void)updateLogInButton{
BOOLtextFieldsNonEmpty=self.usernameTextField.text.length>0&&self.passwordTextField.text.length>0;
BOOLreadyToLogIn=!LoginManager.sharedManager.isLoggingIn&&!self.loggedIn;
self.logInButton.enabled=textFieldsNonEmpty&&readyToLogIn;
}
-(IBAction)logInPressed:(UIButton*)sender{
[[LoginManagersharedManager]
logInWithUsername:self.usernameTextField.text
password:self.passwordTextField.text
success:^{
self.loggedIn=YES;
}failure:^(NSError*error){
[selfpresentError:error];
}];
}
-(void)loggedOut:(NSNotification*)notification{
self.loggedIn=NO;
}
-(void)observeValueForKeyPath:(NSString*)keyPathofObject:(id)objectchange:(NSDictionary*)changecontext:(void*)context{
if
(context==ObservationContext){
[selfupdateLogInButton];
}
else
{
observeValueForKeyPath:keyPathofObject:objectchange:changecontext:context];
}
}
|
… 用RAC表达的话就像下面这样:
-(void)viewDidLoad{
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){
@(username.length>0&&password.length>0&&!loggingIn.boolValue&&!loggedIn.boolValue);
}];
[[self.logInButtonrac_signalForControlEvents:UIControlEventTouchUpInside]subscribeNext:^(UIButton*sender){
@strongify(self);
RACSignal*loginSignal=[LoginManager.sharedManager
logInWithUsername:self.usernameTextField.text
password:self.passwordTextField.text];
[loginSignalsubscribeError:^(NSError*error){
@strongify(self);
[selfpresentError:error];
}completed:^{
@strongify(self);
self.loggedIn=YES;
}];
}];
mapReplace:@NO];
}
|
连接依赖的操作
依赖经常用在网络请求,当下一个对服务器网络请求需要构建在前一个完成时,可以看一下下面的代码:
[clientlogInWithSuccess:^{
[clientloadCachedMessagesWithSuccess:^(NSArray*messages){
[clientfetchMessagesAfterMessage:messages.lastObjectsuccess:^(NSArray*nextMessages){
);
}failure:^(NSError*error){
[selfpresentError:error];
}];
}failure:^(NSError*error){
[selfpresentError:error];
}];
}failure:^(NSError*error){
[selfpresentError:error];
}];
|
ReactiveCocoa 则让这种模式特别简单:
并行地独立地工作
与独立的数据集并行,然后将它们合并成一个最终的结果在Cocoa中是相当不简单的,并且还经常涉及大量的同步:
__blockNSArray*databaSEObjects;
__blockNSArray*fileContents;
NSOperationQueue*backgroundQueue=[[NSOperationQueuealloc]init];
NSBlockOperation*databaSEOperation=[NSBlockOperationblockOperationWithBlock:^{
databaSEObjects=[databaseClientfetchObjectsMatchingPredicate:predicate];
}];
NSBlockOperation*filesOperation=[NSBlockOperationblockOperationWithBlock:^{
NSMutableArray*filesInProgress=[NSMutableArrayarray];
for
(NSString*path
in
files){
[filesInProgressaddObject:[NSDatadataWithContentsOfFile:path]];
}
fileContents=[filesInProgresscopy];
}];
NSBlockOperation*finishOperation=[NSBlockOperationblockOperationWithBlock:^{
"Doneprocessing"
);
}];
[finishOperationaddDependency:databaSEOperation];
[finishOperationaddDependency:filesOperation];
[backgroundQueueaddOperation:databaSEOperation];
[backgroundQueueaddOperation:filesOperation];
[backgroundQueueaddOperation:finishOperation];
|
上面的代码能够简单地用合成signals来清理和优化:
RACSignal*databaseSignal=[[databaseClient
fetchObjectsMatchingPredicate:predicate]
subscribeOn:[RACSchedulerscheduler]];
RACSignal*fileSignal=[RACSignalstartEagerlyWithScheduler:[RACSchedulerscheduler]block:^(idsubscriber){
NSMutableArray*filesInProgress=[NSMutableArrayarray];
files){
[filesInProgressaddObject:[NSDatadataWithContentsOfFile:path]];
}
[subscribersendNext:[filesInProgresscopy]];
[subscribersendCompleted];
}];
[[RACSignal
combineLatest:@[databaseSignal,fileSignal]
reduce:^id(NSArray*databaSEObjects,NSArray*fileContents){
nil;
}]
subscribeCompleted:^{
);
}];
|
简化集合转换
像map,filter,fold/reduce 这些高级功能在Foundation中是极度缺少的m导致了一些像下面这样循环集中的代码:
RACSequence能够允许Cocoa集合用统一的方式操作:
RACSequence*results=[[strings.rac_sequence
filter:^BOOL(NSString*str){
str.length>=2;
map:^(NSString*str){
[strstringByAppendingString:@
}];
|
系统要求
ReactiveCocoa 要求 OS X 10.8+ 以及 iOS 8.0+.
引入 ReactiveCocoa
增加 RAC 到你的应用中:
1. 增加 ReactiveCocoa 仓库 作为你应用仓库的一个子模块.
2. 从ReactiveCocoa文件夹中运行 script/bootstrap .
3. 拖拽 ReactiveCocoa.xcodeproj 到你应用的 Xcode project 或者 workspace中.
4. 在你应用target的"Build Phases"的选项卡,增加 RAC到 "Link Binary With Libraries"
On iOS,增加 libReactiveCocoa-iOS.a.
On OS X,增加 ReactiveCocoa.framework.
RAC 必须选择"Copy Frameworks" . 假如你没有的话,需要选择"Copy Files"和"Frameworks" .
5. 增加 "$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include"
$(inherited)到 "Header Search Paths" (这需要archive builds,但也没什么影响).
6. For iOS targets,增加 -ObjC 到 "Other Linker Flags" .
7. 假如你增加 RAC到一个project (不是一个workspace),你需要适当的添加RAC target到你应用的"Target Dependencies".
假如你喜欢用CocoaPods,这里有一些慷慨地第三方贡献ReactiveCocoa podspecs.
想看一个用了RAC的工程,check out 独立开发
假如你的工作用RAC是隔离的而不是将其集成到另一个项目,你会想打开ReactiveCocoa.xcworkspace 而不是.xcodeproj.
更多信息
ReactiveCocoa灵感来自.NET的ReactiveExtensions(Rx).Rx的一些原则也能够很好的用在RAC.这里有些好的Rx资源:
RAC和Rx灵感都是来自函数式响应式编程.这里有些关于FRP(functional reactive programming)相关的资源: