ReactiveCocoa2实战
概述为什么要使用RAC?一个怪怪的东西,从Demo看也没有让代码变得更好、更短,相反还造成理解上的困难,真的有必要去学它么?相信这是大多数人在接触RAC时的想法。RAC不是单一功能的模块,它是一个Framework,提供了一整套解决方案。其核心思想是「响应数据的变化」,在这个基础上有了Signal的概念,进而可以帮助减少状态变量(可以参考jspahrsummers的PPT),使用MVVM架构,统一的异步编程模型等等。 为什么RAC更加适合编写Cocoa App?说这个之前,我们先来看下Web前端编程,因为有些相似之处。目前很火的AngularJS有一个很重要的特性:数据与视图绑定。就是当数据变化时,视图不需要额外的处理,便可正确地呈现最新的数据。而这也是RAC的亮点之一。RAC与Cocoa的编程模式,有点像AngularJS和jQuery。所以要了解RAC,需要先在观念上做调整。 以下面这个Cell为例 正常的写法可能是这样,很直观。 -(void)configureWithItem:(HBItem*)item{ self.username.text=item.text; [self.avatarImageViewsetImageWithURL:item.avatarURL]; //其他的一些设置} 但如果用RAC,可能就是这样 -(id)init{ if(self=[superinit]){ @weakify(self); [RACObserve(self,viewModel)subscribeNext:^(HBItemViewModel*viewModel){ @strongify(self); self.username.text=viewModel.item.text; [self.avatarImageViewsetImageWithURL:viewModel.item.avatarURL]; //其他的一些设置 }]; }} 也就是先把数据绑定,接下来只要数据有变化,就会自动响应变化。在这里,每次viewModel改变时,内容就会自动变成该viewModel的内容。 SignalSignal是RAC的核心,为了帮助理解,画了这张简化图 这里的数据源和sendXXX,可以理解为函数的参数和返回值。当Signal处理完数据后,可以向下一个Signal或Subscriber传送数据。可以看到上半部分的两个Signal是冷的(cold),相当于实现了某个函数,但该函数没有被调用。同时也说明了Signal可以被组合使用,比如 当signal被subscribe时,就会处于热(hot)的状态,也就是该函数会被执行。比如上面的第二张图,首先signalA可能发了一个网络请求,拿到结果后,把数据通过 还有,一个signal可以被多个subscriber订阅,这里怕显得太乱就没有画出来,但每次被新的subscriber订阅时,都会导致数据源的处理逻辑被触发一次,这很有可能导致意想不到的结果,需要注意一下。 当数据从signal传送到subscriber时,还可以通过 通过这张图可以看到,这非常像中学时学的函数,比如 有些地方需要注意下,比如把signal作为local变量时,如果没有被subscribe,那么方法执行完后,该变量会被dealloc。但如果signal有被subscribe,那么subscriber会持有该signal,直到signal sendCompleted或sendError时,才会解除持有关系,signal才会被dealloc。 RACCommand
假设有这么个需求:当图片载入完后,分享按钮才可用。那么可以这样: RACSignal*imageAvailableSignal=[RACObserve(self,imageView.image)map:id^(idx){returnx?@YES:@NO}];self.shareButton.rac_command=[[RACCommandalloc]initWithEnabled:imageAvailableSignalsignalBlock:^RACSignal*(idinput){ //dosharelogic}]; 除了与 //ViewModel.m-(instancetype)init{ self=[superinit]; if(self){ void(^updatePinLikeStatus)()=^{ self.pin.likedCount=self.pin.hasLiked?self.pin.likedCount-1:self.pin.likedCount+1; self.pin.hasLiked=!self.pin.hasLiked; }; _likeCommand=[[RACCommandalloc]initWithSignalBlock:^RACSignal*(idinput){ //先展示效果,再发送请求 updatePinLikeStatus(); return[[HBAPIManagersharedManager]likePinWithPinID:self.pin.pinID]; }]; [_likeCommand.errorssubscribeNext:^(idx){ //发生错误时,回滚 updatePinLikeStatus(); }]; } returnself;}//ViewController.m-(void)viewDidLoad{ [superviewDidLoad]; //... @weakify(self); [RACObserve(self,viewModel.hasLiked)subscribeNext:^(idx){ @strongify(self); self.pinLikedCountLabel.text=self.viewModel.likedCount; self.likePinImageView.image=[UIImageimageNamed:self.viewModel.hasLiked?@"pin_liked":@"pin_like"]; }]; UITapGestureRecognizer*tapGesture=[[UITapGestureRecognizeralloc]init]; tapGesture.numberOfTapsRequired=2; [[tapGesturerac_gestureSignal]subscribeNext:^(idx){ [self.viewModel.likeCommandexecute:nil]; }];} 再比如某个App要通过Twitter登录,同时允许取消登录,就可以这么做 (source) _twitterLoginCommand=[[RACCommandalloc]initWithSignalBlock:^(id_){ @strongify(self); return[[self twitterSignInSignal] takeUntil:self.cancelCommand.executionSignals]; }];RAC(self.authenticatedUser)=[self.twitterLoginCommand.executionSignalsswitchToLatest]; 常用的模式map + switchToLatest
如果把这两个结合起来就有意思了,想象这么个场景,当用户在搜索框输入文字时,需要通过网络请求返回相应的hints,每当文字有变动时,需要取消上一次的请求,就可以使用这个配搭。这里用另一个Demo,简单演示一下 NSArray*pins=@[@172230988,@172230947,@172230899,@172230777,@172230707];__blockNSIntegerindex=0;RACSignal*signal=[[[[RACSignalinterval:0.1onScheduler:[RACSchedulerscheduler]] take:pins.count] map:^id(idvalue){ return[[[HBAPIManagersharedManager]fetchPinWithPinID:[pins[index++]intValue]]doNext:^(idx){ NSLog(@"这里只会执行一次"); }]; }] switchToLatest];[signalsubscribeNext:^(HBPin*pin){ NSLog(@"pinID:%d",pin.pinID);}completed:^{ NSLog(@"completed");}];//output//2014-06-0517:40:49.851这里只会执行一次//2014-06-0517:40:49.851pinID:172230707//2014-06-0517:40:49.851completed takeUntil
它的常用场景之一是处理cell的button的点击事件,比如点击Cell的详情按钮,需要push一个VC,就可以这样: [[[cell.detailButton rac_signalForControlEvents:UIControlEventTouchUpInside] takeUntil:cell.rac_prepareForReuseSignal] subscribeNext:^(idx){ //generateandpushViewController}]; 如果不加 替换Delegate出现这种需求,通常是因为需要对Delegate的多个方法做统一的处理,这时就可以造一个signal出来,每次该Delegate的某些方法被触发时,该signal就会 @implementationUISearchDisplayController(RAC)-(RACSignal*)rac_isActiveSignal{ self.delegate=self; RACSignal*signal=objc_getAssociatedObject(self,_cmd); if(signal!=nil)returnsignal; /*Createtwosignalsandmergethem*/ RACSignal*didBeginEditing=[[selfrac_signalForSelector:@selector(searchDisplayControllerDidBeginSearch:) fromProtocol:@protocol(UISearchDisplayDelegate)]mapReplace:@YES]; RACSignal*didEndEditing=[[selfrac_signalForSelector:@selector(searchDisplayControllerDidEndSearch:) fromProtocol:@protocol(UISearchDisplayDelegate)]mapReplace:@NO]; signal=[RACSignalmerge:@[didBeginEditing,didEndEditing]]; objc_setAssociatedObject(self,_cmd,signal,OBJC_ASSOCIATION_RETAIN_NONATOMIC); returnsignal;}@end 代码源于此文 使用ReactiveViewModel的didBecomActiveSignalReactiveViewModel是另一个project, 后面的MVVM中会讲到,通常的做法是在VC里设置VM的 RACSubject的使用场景一般不推荐使用 ViewModel一般会有多个 //HBCViewModel.h#import"RVMViewModel.h"@classRACSubject;@interfaceHBCViewModel:RVMViewModel@property(nonatomic)RACSubject*errors;@end//HBCViewModel.m#import"HBCViewModel.h"#import<ReactiveCocoa.h>@implementationHBCViewModel-(instancetype)init{ self=[superinit]; if(self){ _errors=[RACSubjectsubject]; } returnself;}-(void)dealloc{ [_errorssendCompleted];}@end//SomeOtherViewModelinheritHBCViewModel-(instancetype)init{ _fetchLatestCommand=[RACCommandalloc]initWithSignalBlock:^RACSignal*(idinput){ //fetchlatestdata }]; _fetchMoreCommand=[RACCommandalloc]initWithSignalBlock:^RACSignal*(idinput){ //fetchmoredata }]; [self.didBecomeActiveSignalsubscribeNext:^(idx){ [_fetchLatestCommandexecute:nil]; }]; [[RACSignal merge:@[ _fetchMoreCommand.errors,_fetchLatestCommand.errors ]]subscribe:self.errors];} rac_signalForSelector
[[selfrac_signalForSelector:@selector(scrollViewDidEndDecelerating:)fromProtocol:@protocol(UIScrollViewDelegate)]subscribeNext:^(RACTuple*tuple){ //dosomething}];[[selfrac_signalForSelector:@selector(scrollViewDidScroll:)fromProtocol:@protocol(UIScrollViewDelegate)]subscribeNext:^(RACTuple*tuple){ //dosomething}];self.scrollView.delegate=nil;self.scrollView.delegate=self; 注意,这里的delegate需要先设置为nil,再设置为self,而不能直接设置为self,如果self已经是该scrollView的Delegate的话。 有时,我们想对selector的返回值做一些处理,但很遗憾RAC不支持,如果真的有需要的话,可以使用Aspects MVVM这是一个大话题,如果有耐心,且英文还不错的话,可以看一下Cocoa Samurai的这两篇文章。PS: Facebook Paper就是基于MVVM构建的。 MVVM是Model-View-ViewModel的简称,它们之间的关系如下 可以看到View(其实是ViewController)持有ViewModel,这样做的好处是ViewModel更加独立且可测试,ViewModel里不应包含任何View相关的元素,哪怕换了一个View也能正常工作。而且这样也能让View/ViewController「瘦」下来。 ViewModel主要做的事情是作为View的数据源,所以通常会包含网络请求。 或许你会疑惑,ViewController哪去了?在MVVM的世界里,ViewController已经成为了View的一部分。它的主要职责是将VM与View绑定、响应VM数据的变化、调用VM的某个方法、与其他的VC打交道。 而RAC为MVVM带来很大的便利,比如 以下面这个需求为例,要求大图滑动结束时,底部的缩略图滚动到对应的位置,并高亮该缩略图;同时底部的缩略图被选中时,大图也要变成该缩略图的大图。 我的思路是横向滚动的大图是一个collectionView,该collectionView是当前页面VC的一个property。底部可以滑动的缩略图是一个childVC的collectionView,这两个collectionView共用一套VM,并且各自RACObserve感兴趣的property。 比如大图滑到下一页时,会改变VM的indexPath属性,而底部的collectionView所在的VC正好对该indexPath感兴趣,只要indexPath变化就滚动到相应的Item //childVC-(void)viewDidLoad{ [superviewDidLoad]; @weakify(self); [RACObserve(self,viewModel.indexPath)subscribeNext:^(NSNumber*index){ @strongify(self); [selfscrollToIndexPath]; }];}-(void)scrollToIndexPath{ if(self.collectionView.subviews.count){ NSIndexPath*indexPath=self.viewModel.indexPath; [self.collectionViewscrollToItemAtIndexPath:indexPathatScrollPosition:UICollectionViewScrollPositionCenteredHorizontallyanimated:YES]; [self.collectionView.subviewsenumerateObjectsUsingBlock:^(UIView*view,NSUIntegeridx,BOOL*stop){ view.layer.borderWidth=0; }]; UIView*view=[self.collectionViewcellForItemAtIndexPath:indexPath]; view.layer.borderWidth=kHBPinsNaviThumbnailPadding; view.layer.borderColor=[UIColorwhiteColor].CGColor; }} 当点击底部的缩略图时,上面的大图也要做出变化,也同样可以通过RACObserve indexPath来实现 //PinsViewController.m-(void)viewDidLoad{ [superviewDidLoad]; @weakify(self); [[RACObserve(self,viewModel.indexPath) skip:1] subscribeNext:^(NSIndexPath*indexPath){ @strongify(self); [self.collectionViewscrollToItemAtIndexPath:indexPathatScrollPosition:UICollectionViewScrollPositionCenteredHorizontallyanimated:YES]; }];} 这里有一个小技巧,当Cell里的元素比较复杂时,我们可以给Cell也准备一个ViewModel,这个CellViewModel可以由上一层的ViewModel提供,这样Cell如果需要相应的数据,直接跟CellViewModel要即可,CellViewModel也可以包含一些command,比如likeCommand。假如点击Cell时,要做一些处理,也很方便。 //CellViewModel已经在ViewModel里准备好了-(UICollectionViewCell*)collectionView:(UICollectionView*)collectionViewcellForItemAtIndexPath:(NSIndexPath*)indexPath{ HBPinsCell*cell=[collectionViewdequeueReusableCellWithReuseIdentifier:cellIdentifierforIndexPath:indexPath]; cell.viewModel=self.viewModel.cellViewModels[indexPath.row]; returncell;}-(void)collectionView:(UICollectionView*)collectionViewdidSelectItemAtIndexPath:(NSIndexPath*)indexPath{ HBCellViewModel*cellViewModel=self.viewModel.cellViewModels[indexPath.row]; //对cellViewModel执行某些操作,因为Cell已经与cellViewModel绑定,所以cellViewModel的改变也会反映到Cell上 //或拿到cellViewModel的数据来执行某些操作} ViewModel中signal,property,command的使用初次使用RAC+MVVM时,往往会疑惑,什么时候用signal,什么时候用property,什么时候用command? 一般来说可以使用property的就直接使用,没必要再转换成signal,外部RACObserve即可。使用signal的场景一般是涉及到多个property或多个signal合并为一个signal。command往往与UIControl/网络请求挂钩。 常见场景的处理检查本地缓存,如果失效则去请求网络数据并缓存到本地来源 -(RACSignal*)loadData{ return[[RACSignal createSignal:^(id<RACSubscriber>subscriber){ //Ifthecacheisvalidthenwecanjustimmediatelysendthe //cacheddataandbedone. if(self.cacheValid){ [subscribersendNext:self.cachedData]; [subscribersendCompleted]; }else{ [subscribersendError:self.staleCacheError]; } }] //Dothesubscriptionworkonsomerandomscheduler,offthemain //thread. subscribeOn:[RACSchedulerscheduler]];}-(void)update{ [[[[self loadData] //Catchtheerrorfrom-loadData.Itmeansourcacheisstale.Update //ourcacheandsaveit. catch:^(NSError*error){ return[[selfupdateCachedData]doNext:^(iddata){ [selfcacheData:data]; }]; }] //Ourworkupuntilnowhasbeenonabackgroundscheduler.Getour //resultsdeliveredonthemainthreadsowecandoUIwork. deliverOn:RACScheduler.mainThreadScheduler] subscribeNext:^(iddata){ //UpdateyourUIbasedon`data`. //Updateagainafter`updateInterval`secondshavepassed. [[RACSignalinterval:updateInterval]take:1]subscribeNext:^(id_){ [selfupdate]; }]; }];} 检测用户名是否可用来源 -(void)setupUsernameAvailabilityChecking{ RAC(self,availabilityStatus)=[[[RACObserve(self.userTemplate,username) throttle:kUsernameCheckThrottleInterval]//throttle表示interval时间内如果有sendNext,则放弃该nextValue map:^(NSString*username){ if(username.length==0)return[RACSignalreturn:@(UsernameAvailabilityCheckStatusEmpty)]; return[[[[[FIBAPIClientsharedInstance] getUsernameAvailabilityFor:usernameignoreCache:NO] map:^(NSDictionary*result){ NSNumber*existsNumber=result[@"exists"]; if(!existsNumber)return@(UsernameAvailabilityCheckStatusFailed); UsernameAvailabilityCheckStatusstatus=[existsNumberboolValue]?UsernameAvailabilityCheckStatusUnavailable:UsernameAvailabilityCheckStatusAvailable; return@(status); }] catch:^(NSError*error){ return[RACSignalreturn:@(UsernameAvailabilityCheckStatusFailed)]; }]startWith:@(UsernameAvailabilityCheckStatusChecking)]; }] switchToLatest];} 可以看到这里也使用了
使用takeUntil:来处理Cell的button点击这个上面已经提到过了。 token过期后自动获取新的开发APIClient时,会用到AccessToken,这个Token过一段时间会过期,需要去请求新的Token。比较好的用户体验是当token过期后,自动去获取新的Token,拿到后继续上一次的请求,这样对用户是透明的。 RACSignal*requestSignal=[RACSignalcreateSignal:^RACDisposable*(id<RACSubscriber>subscriber){ //supposefirsttimesendrequest,accesstokenisexpiredorinvalid //andnexttimeitiscorrect. //theblockwillbetriggeredtwice. staticBOOLisFirstTime=0; NSString*url=@"http://httpbin.org/ip"; if(!isFirstTime){ url=@"http://nonexists.com/error"; isFirstTime=1; } NSLog(@"url:%@",url); [[AFHTTPRequestOperationManagermanager]GET:urlparameters:nilsuccess:^(AFHTTPRequestOperation*operation,idresponSEObject){ [subscribersendNext:responSEObject]; [subscribersendCompleted]; }failure:^(AFHTTPRequestOperation*operation,NSError*error){ [subscribersendError:error]; }]; returnnil; }]; self.statusLabel.text=@"sendingrequest..."; [[requestSignalcatch:^RACSignal*(NSError*error){ self.statusLabel.text=@"oops,invalidaccesstoken"; //simulatenetworkrequest,andwefetchtherightaccesstoken return[[RACSignalcreateSignal:^RACDisposable*(id<RACSubscriber>subscriber){ doubledelayInSeconds=1.0; dispatch_time_tpopTime=dispatch_time(DISPATCH_TIME_NOW,(int64_t)(delayInSeconds*NSEC_PER_SEC)); dispatch_after(popTime,dispatch_get_main_queue(),^(void){ [subscribersendNext:@YES]; [subscribersendCompleted]; }); returnnil; }]concat:requestSignal]; }]subscribeNext:^(idx){ if([xisKindOfClass:[NSDictionaryclass]]){ self.statusLabel.text=[NSStringstringWithFormat:@"result:%@",x[@"origin"]]; } }completed:^{ NSLog(@"completed"); }]; 注意事项RAC我自己感觉遇到的几个难点是: 1) 理解RAC的理念。 2) 熟悉常用的API。3) 针对某些特定的场景,想出比较合理的RAC处理方式。不过看多了,写多了,想多了就会慢慢适应。下面是我在实践过程中遇到的一些小坑。 ReactiveCocoaLayout有时Cell的内容涉及到动态的高度,就会想到用Autolayout来布局,但RAC已经为我们准备好了ReactiveCocoaLayout,所以我想不妨就拿来用一下。
所以 调试刚开始写RAC时,往往会遇到这种情况,满屏的调用栈信息都是RAC的,要找出真正出现问题的地方不容易。曾经有一次在使用 不过写多了之后,一般不太会犯这种低级错误。 strongify / weakify dance因为RAC很多操作都是在Block中完成的,这块最常见的问题就是在block直接把self拿来用,造成block和self的retain cycle。所以需要通过 有些地方很容易被忽略,比如 小结以上是我在做花瓣客户端和side project时总结的一些经验,但愿能带来一些帮助,有误的地方也欢迎指正和探讨。 推荐一下jspahrsummers的这个project,虽然是用RAC3.0写的,但很多理念也可以用到RAC2上面。 最后感谢Github的iOS工程师们,感谢你们带来了RAC,以及在Issues里的耐心解答。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- ruby-on-rails – Minitest:相关AR模型的装置导
- Cache Handler Class
- 15款Cocos2d-x游戏源码
- C++ 数据结构之布隆过滤器
- Access to the path "Library\UnityAsse
- 读取xml文件里switch节点的IP和设备信息,ping设
- c# – NegotiateStream拒绝非本地管理员的客户端
- quick-cocos2d-x3.3在windows下编译release版报错
- ruby-on-rails – 推送Heroku,json和ruby 1.9.2时
- React Native 向iOS项目中添加React Native支持