网站空间域名续费合同,自己做个网站用什么软件好,做淘宝类网站,福州医社保增减员在什么网站做概述 为什么要使用RAC#xff1f;一个怪怪的东西#xff0c;从Demo看也没有让代码变得更好、更短#xff0c;相反还造成理解上的困难#xff0c;真的有必要去学它么#xff1f;相信这是大多数人在接触RAC时的想法。RAC不是单一功能的模块#xff0c;它是一个Framework 一个怪怪的东西从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.avatarImageView setImageWithURL: item.avatarURL]; // 其他的一些设置 } 但如果用RAC可能就是这样 - (id)init { if (self [super init]) { weakify(self); [RACObserve(self, viewModel) subscribeNext:^(HBItemViewModel *viewModel) { strongify(self); self.username.text viewModel.item.text; [self.avatarImageView setImageWithURL: viewModel.item.avatarURL]; // 其他的一些设置 }]; } } 也就是先把数据绑定接下来只要数据有变化就会自动响应变化。在这里每次viewModel改变时内容就会自动变成该viewModel的内容。 Signal Signal是RAC的核心为了帮助理解画了这张简化图 这里的数据源和sendXXX可以理解为函数的参数和返回值。当Signal处理完数据后可以向下一个Signal或Subscriber传送数据。可以看到上半部分的两个Signal是冷的(cold)相当于实现了某个函数但该函数没有被调用。同时也说明了Signal可以被组合使用比如RACSignal *signalB [signalA map:^id(id x){return x}]或RACSignal *signalB [signalA take:1]等等。 当signal被subscribe时就会处于热(hot)的状态也就是该函数会被执行。比如上面的第二张图首先signalA可能发了一个网络请求拿到结果后把数据通过sendNext方法传递到下一个signalsignalB可以根据需要做进一步处理比如转换成相应的Model转换完后再sendNext到subscribersubscriber拿到数据后再改变ViewModel同时因为View已经绑定了ViewModel所以拿到的数据会自动在View里呈现。 还有一个signal可以被多个subscriber订阅这里怕显得太乱就没有画出来但每次被新的subscriber订阅时都会导致数据源的处理逻辑被触发一次这很有可能导致意想不到的结果需要注意一下。 当数据从signal传送到subscriber时还可以通过doXXX来做点事情比如打印数据。 通过这张图可以看到这非常像中学时学的函数比如 f(x) y某一个函数的输出又可以作为另一个函数的输入比如 f(f(x)) z这也正是「函数响应式编程」(FRP)的核心。 有些地方需要注意下比如把signal作为local变量时如果没有被subscribe那么方法执行完后该变量会被dealloc。但如果signal有被subscribe那么subscriber会持有该signal直到signal sendCompleted或sendError时才会解除持有关系signal才会被dealloc。 RACCommand RACCommand是RAC很重要的组成部分可以节省很多时间并且让你的App变得更Robust这篇文章可以帮助你更深入的理解这里简单做一下介绍。 RACCommand 通常用来表示某个Action的执行比如点击Button。它有几个比较重要的属性executionSignals / errors / executing。 1、executionSignals是signal of signals如果直接subscribe的话会得到一个signal而不是我们想要的value所以一般会配合switchToLatest。 2、errors。跟正常的signal不一样RACCommand的错误不是通过sendError来实现的而是通过errors属性传递出来的。 3、executing表示该command当前是否正在执行。 假设有这么个需求当图片载入完后分享按钮才可用。那么可以这样 RACSignal *imageAvailableSignal [RACObserve(self, imageView.image) map:id^(id x){return x ? YES : NO}]; self.shareButton.rac_command [[RACCommand alloc] initWithEnabled:imageAvailableSignal signalBlock:^RACSignal *(id input) { // do share logic }]; 除了与UIControl绑定之外也可以手动执行某个command比如双击图片点赞就可以这么实现。 // ViewModel.m - (instancetype)init { self [super init]; 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 [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) { // 先展示效果再发送请求 updatePinLikeStatus(); return [[HBAPIManager sharedManager] likePinWithPinID:self.pin.pinID]; }]; [_likeCommand.errors subscribeNext:^(id x) { // 发生错误时回滚 updatePinLikeStatus(); }]; } return self; } // ViewController.m - (void)viewDidLoad { [super viewDidLoad]; // ... weakify(self); [RACObserve(self, viewModel.hasLiked) subscribeNex:^(id x){ strongify(self); self.pinLikedCountLabel.text self.viewModel.likedCount; self.likePinImageView.image [UIImage imageNamed:self.viewModel.hasLiked ? pin_liked : pin_like]; }]; UITapGestureRecognizer *tapGesture [[UITapGestureRecognizer alloc] init]; tapGesture.numberOfTapsRequired 2; [[tapGesture rac_gestureSignal] subscribeNext:^(id x) { [self.viewModel.likeCommand execute:nil]; }]; } 再比如某个App要通过Twitter登录同时允许取消登录就可以这么做 (source) _twitterLoginCommand [[RACCommand alloc] initWithSignalBlock:^(id _) { strongify(self); return [[self twitterSignInSignal] takeUntil:self.cancelCommand.executionSignals]; }]; RAC(self.authenticatedUser) [self.twitterLoginCommand.executionSignals switchToLatest]; 常用的模式 map switchToLatest switchToLatest: 的作用是自动切换signal of signals到最后一个比如之前的command.executionSignals就可以使用switchToLatest:。 map:的作用很简单对sendNext的value做一下处理返回一个新的值。 如果把这两个结合起来就有意思了想象这么个场景当用户在搜索框输入文字时需要通过网络请求返回相应的hints每当文字有变动时需要取消上一次的请求就可以使用这个配搭。这里用另一个Demo简单演示一下 NSArray *pins [172230988, 172230947, 172230899, 172230777, 172230707]; __block NSInteger index 0; RACSignal *signal [[[[RACSignal interval:0.1 onScheduler:[RACScheduler scheduler]] take:pins.count] map:^id(id value) { return [[[HBAPIManager sharedManager] fetchPinWithPinID:[pins[index] intValue]] doNext:^(id x) { NSLog(这里只会执行一次); }]; }] switchToLatest]; [signal subscribeNext:^(HBPin *pin) { NSLog(pinID:%d, pin.pinID); } completed:^{ NSLog(completed); }]; // output // 2014-06-05 17:40:49.851 这里只会执行一次 // 2014-06-05 17:40:49.851 pinID:172230707 // 2014-06-05 17:40:49.851 completed takeUntil takeUntil:someSignal 的作用是当someSignal sendNext时当前的signal就sendCompletedsomeSignal就像一个拳击裁判哨声响起就意味着比赛终止。 它的常用场景之一是处理cell的button的点击事件比如点击Cell的详情按钮需要push一个VC就可以这样 [[[cell.detailButton rac_signalForControlEvents:UIControlEventTouchUpInside] takeUntil:cell.rac_prepareForReuseSignal] subscribeNext:^(id x) { // generate and push ViewController }]; 如果不加takeUntil:cell.rac_prepareForReuseSignal那么每次Cell被重用时该button都会被addTarget:selector。 替换Delegate 出现这种需求通常是因为需要对Delegate的多个方法做统一的处理这时就可以造一个signal出来每次该Delegate的某些方法被触发时该signal就会sendNext。 implementation UISearchDisplayController (RAC) - (RACSignal *)rac_isActiveSignal { self.delegate self; RACSignal *signal objc_getAssociatedObject(self, _cmd); if (signal ! nil) return signal; /* Create two signals and merge them */ RACSignal *didBeginEditing [[self rac_signalForSelector:selector(searchDisplayControllerDidBeginSearch:) fromProtocol:protocol(UISearchDisplayDelegate)] mapReplace:YES]; RACSignal *didEndEditing [[self rac_signalForSelector:selector(searchDisplayControllerDidEndSearch:) fromProtocol:protocol(UISearchDisplayDelegate)] mapReplace:NO]; signal [RACSignal merge:[didBeginEditing, didEndEditing]]; objc_setAssociatedObject(self, _cmd, signal, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return signal; } end 代码源于此文 使用ReactiveViewModel的didBecomActiveSignal ReactiveViewModel是另一个project 后面的MVVM中会讲到通常的做法是在VC里设置VM的active属性(RVMViewModel自带该属性)然后在VM里subscribeNext didBecomActiveSignal比如当Active时获取TableView的最新数据。 RACSubject的使用场景 一般不推荐使用RACSubject因为它过于灵活滥用的话容易导致复杂度的增加。但有一些场景用一下还是比较方便的比如ViewModel的errors。 ViewModel一般会有多个RACCommand那这些commands如果出现error了该如何处理呢比较方便的方法如下 // HBCViewModel.h #import RVMViewModel.h class RACSubject; interface HBCViewModel : RVMViewModel property (nonatomic) RACSubject *errors; end // HBCViewModel.m #import HBCViewModel.h #import ReactiveCocoa.h implementation HBCViewModel - (instancetype)init { self [super init]; if (self) { _errors [RACSubject subject]; } return self; } - (void)dealloc { [_errors sendCompleted]; } end // Some Other ViewModel inherit HBCViewModel - (instancetype)init { _fetchLatestCommand [RACCommand alloc] initWithSignalBlock:^RACSignal *(id input){ // fetch latest data }]; _fetchMoreCommand [RACCommand alloc] initWithSignalBlock:^RACSignal *(id input){ // fetch more data }]; [self.didBecomeActiveSignal subscribeNext:^(id x) { [_fetchLatestCommand execute:nil]; }]; [[RACSignal merge:[ _fetchMoreCommand.errors, _fetchLatestCommand.errors ]] subscribe:self.errors]; } rac_signalForSelector rac_signalForSelector: 这个方法会返回一个signal当selector执行完时会sendNext也就是当某个方法调用完后再额外做一些事情。用在category会比较方便因为Category重写父类的方法时不能再通过[super XXX]来调用父类的方法当然也可以手写Swizzle来实现不过有了rac_signalForSelector:就方便多了。 rac_signalForSelector: fromProtocol: 可以直接实现对protocol的某个方法的实现听着有点别扭呢比如我们想实现UIScrollViewDelegate的某些方法可以这么写 [[self rac_signalForSelector:selector(scrollViewDidEndDecelerating:) fromProtocol:protocol(UIScrollViewDelegate)] subscribeNext:^(RACTuple *tuple) { // do something }]; [[self rac_signalForSelector:selector(scrollViewDidScroll:) fromProtocol:protocol(UIScrollViewDelegate)] subscribeNext:^(RACTuple *tuple) { // do something }]; 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带来很大的便利比如RACCommand, UIKit的RAC Extension等等。使用MVVM不一定能减少代码量但能降低代码的复杂度。 以下面这个需求为例要求大图滑动结束时底部的缩略图滚动到对应的位置并高亮该缩略图同时底部的缩略图被选中时大图也要变成该缩略图的大图。 我的思路是横向滚动的大图是一个collectionView该collectionView是当前页面VC的一个property。底部可以滑动的缩略图是一个childVC的collectionView这两个collectionView共用一套VM并且各自RACObserve感兴趣的property。 比如大图滑到下一页时会改变VM的indexPath属性而底部的collectionView所在的VC正好对该indexPath感兴趣只要indexPath变化就滚动到相应的Item // childVC - (void)viewDidLoad { [super viewDidLoad]; weakify(self); [RACObserve(self, viewModel.indexPath) subscribeNext:^(NSNumber *index) { strongify(self); [self scrollToIndexPath]; }]; } - (void)scrollToIndexPath { if (self.collectionView.subviews.count) { NSIndexPath *indexPath self.viewModel.indexPath; [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES]; [self.collectionView.subviews enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) { view.layer.borderWidth 0; }]; UIView *view [self.collectionView cellForItemAtIndexPath:indexPath]; view.layer.borderWidth kHBPinsNaviThumbnailPadding; view.layer.borderColor [UIColor whiteColor].CGColor; } } 当点击底部的缩略图时上面的大图也要做出变化也同样可以通过RACObserve indexPath来实现 // PinsViewController.m - (void)viewDidLoad { [super viewDidLoad]; weakify(self); [[RACObserve(self, viewModel.indexPath) skip:1] subscribeNext:^(NSIndexPath *indexPath) { strongify(self); [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES]; }]; } 这里有一个小技巧当Cell里的元素比较复杂时我们可以给Cell也准备一个ViewModel这个CellViewModel可以由上一层的ViewModel提供这样Cell如果需要相应的数据直接跟CellViewModel要即可CellViewModel也可以包含一些command比如likeCommand。假如点击Cell时要做一些处理也很方便。 // CellViewModel已经在ViewModel里准备好了 - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { HBPinsCell *cell [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath]; cell.viewModel self.viewModel.cellViewModels[indexPath.row]; return cell; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { HBCellViewModel *cellViewModel self.viewModel.cellViewModels[indexPath.row]; // 对cellViewModel执行某些操作因为Cell已经与cellViewModel绑定所以cellViewModel的改变也会反映到Cell上 // 或拿到cellViewModel的数据来执行某些操作 } ViewModel中signal, property, command的使用 初次使用RACMVVM时往往会疑惑什么时候用signal什么时候用property什么时候用command 一般来说可以使用property的就直接使用没必要再转换成signal外部RACObserve即可。使用signal的场景一般是涉及到多个property或多个signal合并为一个signal。command往往与UIControl/网络请求挂钩。 常见场景的处理 检查本地缓存如果失效则去请求网络数据并缓存到本地 来源 - (RACSignal *)loadData { return [[RACSignal createSignal:^(idRACSubscriber subscriber) { // If the cache is valid then we can just immediately send the // cached data and be done. if (self.cacheValid) { [subscriber sendNext:self.cachedData]; [subscriber sendCompleted]; } else { [subscriber sendError:self.staleCacheError]; } }] // Do the subscription work on some random scheduler, off the main // thread. subscribeOn:[RACScheduler scheduler]]; } - (void)update { [[[[self loadData] // Catch the error from -loadData. It means our cache is stale. Update // our cache and save it. catch:^(NSError *error) { return [[self updateCachedData] doNext:^(id data) { [self cacheData:data]; }]; }] // Our work up until now has been on a background scheduler. Get our // results delivered on the main thread so we can do UI work. deliverOn:RACScheduler.mainThreadScheduler] subscribeNext:^(id data) { // Update your UI based on data. // Update again after updateInterval seconds have passed. [[RACSignal interval:updateInterval] take:1] subscribeNext:^(id _) { [self update]; }]; }]; } 检测用户名是否可用 来源 - (void)setupUsernameAvailabilityChecking { RAC(self, availabilityStatus) [[[RACObserve(self.userTemplate, username) throttle:kUsernameCheckThrottleInterval] //throttle表示interval时间内如果有sendNext则放弃该nextValue map:^(NSString *username) { if (username.length 0) return [RACSignal return:(UsernameAvailabilityCheckStatusEmpty)]; return [[[[[FIBAPIClient sharedInstance] getUsernameAvailabilityFor:username ignoreCache:NO] map:^(NSDictionary *result) { NSNumber *existsNumber result[exists]; if (!existsNumber) return (UsernameAvailabilityCheckStatusFailed); UsernameAvailabilityCheckStatus status [existsNumber boolValue] ? UsernameAvailabilityCheckStatusUnavailable : UsernameAvailabilityCheckStatusAvailable; return (status); }] catch:^(NSError *error) { return [RACSignal return:(UsernameAvailabilityCheckStatusFailed)]; }] startWith:(UsernameAvailabilityCheckStatusChecking)]; }] switchToLatest]; } 可以看到这里也使用了map switchToLatest模式这样就可以自动取消上一次的网络请求。 startWith的内部实现是concat这里表示先将状态置为checking然后再根据网络请求的结果设置状态。 使用takeUntil:来处理Cell的button点击 这个上面已经提到过了。 token过期后自动获取新的 开发APIClient时会用到AccessToken这个Token过一段时间会过期需要去请求新的Token。比较好的用户体验是当token过期后自动去获取新的Token拿到后继续上一次的请求这样对用户是透明的。 RACSignal *requestSignal [RACSignal createSignal:^RACDisposable *(idRACSubscriber subscriber) { // suppose first time send request, access token is expired or invalid // and next time it is correct. // the block will be triggered twice. static BOOL isFirstTime 0; NSString *url http://httpbin.org/ip; if (!isFirstTime) { url http://nonexists.com/error; isFirstTime 1; } NSLog(url:%, url); [[AFHTTPRequestOperationManager manager] GET:url parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { [subscriber sendNext:responseObject]; [subscriber sendCompleted]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [subscriber sendError:error]; }]; return nil; }]; self.statusLabel.text sending request...; [[requestSignal catch:^RACSignal *(NSError *error) { self.statusLabel.text oops, invalid access token; // simulate network request, and we fetch the right access token return [[RACSignal createSignal:^RACDisposable *(idRACSubscriber subscriber) { double delayInSeconds 1.0; dispatch_time_t popTime dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ [subscriber sendNext:YES]; [subscriber sendCompleted]; }); return nil; }] concat:requestSignal]; }] subscribeNext:^(id x) { if ([x isKindOfClass:[NSDictionary class]]) { self.statusLabel.text [NSString stringWithFormat:result:%, x[origin]]; } } completed:^{ NSLog(completed); }]; 注意事项 RAC我自己感觉遇到的几个难点是: 1) 理解RAC的理念。 2) 熟悉常用的API。3) 针对某些特定的场景想出比较合理的RAC处理方式。不过看多了写多了想多了就会慢慢适应。下面是我在实践过程中遇到的一些小坑。 ReactiveCocoaLayout 有时Cell的内容涉及到动态的高度就会想到用Autolayout来布局但RAC已经为我们准备好了ReactiveCocoaLayout所以我想不妨就拿来用一下。 ReactiveCocoaLayout的使用好比「批地」和「盖房」先通过insetWidth:height:nullRect从某个View中划出一小块拿到之后还可以通过divideWithAmount:padding:fromEdge 再分成两块或sliceWithAmount:fromEdge再分出一块。这些方法返回的都是signal所以可以通过RAC(self.view, frame) someRectSignal 这样来实现绑定。但在实践中发现性能不是很好多批了几块地就容易造成主线程卡顿。 所以ReactiveCocoaLayout最好不用或少用。 调试 刚开始写RAC时往往会遇到这种情况满屏的调用栈信息都是RAC的要找出真正出现问题的地方不容易。曾经有一次在使用[RACSignal combineLatest: reduce:^id{}]时忘了在Block里返回value而Xcode也没有提示warning然后就是莫名其妙地挂起了跳到了汇编上也没有调用栈信息这时就只能通过最古老的注释代码的方式来找到问题的根源。 不过写多了之后一般不太会犯这种低级错误。 strongify / weakify dance 因为RAC很多操作都是在Block中完成的这块最常见的问题就是在block直接把self拿来用造成block和self的retain cycle。所以需要通过strongify和weakify来消除循环引用。 有些地方很容易被忽略比如RACObserve(thing, keypath)看上去并没有引用self所以在subscribeNext时就忘记了weakify/strongify。但事实上RACObserve总是会引用self即使target不是self所以只要有RACObserve的地方都要使用weakify/strongify。 转自http://www.cocoachina.com/industry/20140609/8737.html