打算在项目中大面积使用RAC来开发,所以整理一些常用的实践范例和比较完整的api说明方便开发时随时查阅
函数反应式编程是声明式编程的子编程范式之一
需要满足两个条件
Objective-c里使用block作为函数
[array enumerateObjectsUsingBlock:^(NSNumber *number, NSUInteger idx, BOOL *stop) { NSLog(@"%@",number); }];
NSArray * mappedArray = [array rx_mapWithBlock:^id(id each){ return @(pow([each integerValue],2)); }];
NSArray *filteredArray = [array rx_filterWithBlock:^BOOL(id each){ return ([each integerValue] % 2 == 0); }]
[[array rx_mapWithBlock:^id (id each){ return [each stringValue]; }] rx_foldInitialValue:@"" block:^id (id memo , id each){ return [memo stringByAppendingString:each]; }];
NSArray *array = @[ @1, @2, @3 ]; RACSequence * stream = [array rac_sequence]; //RACSequence是一个RACStream的子类。 [stream map:^id (id value){ return @(pow([value integerValue], 2)); }]; //RACSequence有一个方法返回数组:array NSLog(@"%@",[stream array]); //避免污染变量的作用域 NSLog(@"%@",[[[array rac_sequence] map:^id (id value){ return @(pow([value integerValue], 2)); }] array]);
NSLog(@"%@", [[[array rac_sequence] filter:^BOOL (id value){ return [value integerValue] % 2 == 0; }] array]);
NSLog(@"%@",[[[array rac_sequence] map:^id (id value){ return [value stringValue]; }] foldLeftWithStart:@"" reduce:^id (id accumulator, id value){ return [accumulator stringByAppendingString:value]; }]);
RACSignal * validEmailSignal = [self.textField.rac_textSignal map:^id (NSString *value){ return @([value rangeOfString:@"@"].location != NSNotFound); }]; RAC(self.button, enabled) = validEmailSignal; RAC(self.textField, textColor) = [validEmailSignal map: ^id (id value){ if([value boolValue]){ return [UIColor greenColor]; }else{ return [UIColor redColor]; } }];
比较好的一个完整的RAC实践的例子: https://github.com/ashfurrow/FunctionalReactivePixels
+ (RACSignal *)importPhotos{ RACReplaySubject * subject = [RACReplaySubject subject]; NSURLRequest * request = [self popularURLRequest]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError){ if (data) { id results = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; [subject sendNext:[[[results[@"photos"] rac_sequence] map:^id(NSDictionary *photoDictionary){ FRPPhotoModel * model = [FRPPhotoModel new]; [self configurePhotoModel:model withDictionary:photoDictionary]; [self downloadThumbnailForPhotoModel:model]; return model; }] array]]; [subject sendCompleted]; } else{ [subject sendError:connectionError]; } }]; return subject; }
+ (NSString *)urlForImageSize:(NSInteger)size inDictionary:(NSArray *)array{ return [[[[[array rac_sequence] filter:^ BOOL (NSDictionary * value){ return [value[@"size"] integerValue] == size; }] map:^id (id value){ return value[@"url"]; }] array] firstObject]; }
观察model里的图片数据,进行为空过滤判断,将data转为UIImage,再把绑定新信号的值给对象的关键路径
- (void)setPhotoModel:(FRPPhotoModel *)photoModel{ self.subscription = [[[RACObserver(photoModel, thumbnailData) filter:^ BOOL (id value){ return value != nil; }] map:^id (id value){ return [UIImage imageWithData:value]; }] setKeyPath:@keypath(self.imageView, image) onObject:self.imageView]; }
- (void)perpareForReuse { [super prepareForReuse]; [self.subscription dispose], self.subscription = nil; }
//注意:你必须retain这个delegate对象,否则他们将会被释放,你将会得到一个EXC_BAD_ACCESS异常。添加下列私有属性到画廊视图控制器: @property (nonatomic, strong) id collectionViewDelegate; //同时你也需要导入RACDelegateProxy.h,因为他不是ReactiveCocoa的核心部分,不包含在ReactiveCocoa.h中。 RACDelegateProxy *viewControllerDelegate = [[RACDelegateProxy alloc] initWithProtocol:@protocol(FRPFullSizePhotoViewControllerDelegate)]; [[viewControllerDelegate rac_signalForSelector:@selector(userDidScroll:toPhotoAtIndex:) fromProtocol:@protocol(FRPFullSizePhotoViewControllerDelegate)] subscribeNext:^(RACTuple *value){ @strongify(self); [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:[value.second integerValue] inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:NO]; }]; self.collectionViewDelegate = [[RACDelegateProxy alloc] initWithProtocol:@protocol(UICollectionViewDelegate)]; [[self.collectionViewDelegate rac_signalForSelector:@selector(collectionView:didSelectItemAtIndexPath:)] subscribeNext:^(RACTuple *arguments) { @strongify(self); FRPFullSizePhotoViewController *viewController = [[FRPFullSizePhotoViewController alloc] initWithPhotoModels:self.photosArray currentPhotoIndex:[(NSIndexPath *)arguments.second item]]; viewController.delegate = (id<FRPFullSizePhotoViewControllerDelegate>)viewControllerDelegate; [self.navigationController pushViewController:viewController animated:YES]; }];
RAC(self, photosArray) = [[[[FRPPhotoImporter importPhotos] doCompleted:^{ @strongify(self); [self.collectionView reloadData]; }] logError] catchTo:[RACSignal empty]];
+ (RACSignal *)importPhotos { NSURLRequest *request = [self popularURLRequest]; return [[[[[[NSURLConnection rac_sendAsynchronousRequest:request] reduceEach:^id(NSURLResponse *response , NSData *data){ //注意:我们可以用下面的reduceEach:替代使用RACTuple的第一个map:,以便提供编译时检查。 return data; }] deliverOn:[RACScheduler mainThreadScheduler]] map:^id (NSData *data) { id results = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; return [[[results[@"photo"] rac_sequence] map:^id (NSDictionary *photoDictionary) { FRPPhotoModel *model = [FRPPhotoModel new]; [self configurePhotoModel:model withDictionary:photoDictionary]; [self downloadThumbnailForPhotoModel:model]; return model; }] array]; }] publish] autoconnect]; //信号链条最末端的信号操作publish. publish返回一个RACMulitcastConnection,当信号连接上时,他将订阅该接收信号。autoconnect为我们做的是:当它返回的信号被订阅,连接到 该(订阅背后的)信号(underly signal)。 }
信号的信号Signal of signals,一个外部信号包含一个内部信号,在输出信号的subscribeNext:块中订阅内部信号,会引起嵌套麻烦。使用flattenMap后会生成一个新的信号,和先前信号平级,订阅会订阅到返回的新信号里的值。map方法也是创建一个新信号,但是会将返回的信号也当做值,这样就得不到真正需要的值了。
[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^RACStream *(id value) { return [self signInSignal]; }] subscribeNext:^(id x) { //x NSLog(@"Sign in result: %@", x); }];
不同信号顺序链接,程序需要等待前一个信号发出完成事件(sendCompleted),然后再订阅下一个信号(then)
- (RACSignal *)requestAccessToTwitterSignal { // 定义一个错误,如果用户拒绝访问则发送 NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorAccessDenied userInfo:nil]; // 创建并返回信号 @weakify(self) return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { // 请求访问twitter @strongify(self) [self.accountStore requestAccessToAccountsWithType:self.twitterAccountType options:nil completion:^(BOOL granted, NSError *error) { // 处理响应 if (!granted) { [subscriber sendError:accessError]; } else { [subscriber sendNext:nil]; [subscriber sendCompleted]; } }]; return nil; }]; } //throttle可以避免连续输入造成的不必要的请求,then会忽略前一个信号的值,底层的实现是先过滤之前信号发的值,再使用concat连接then返回的信号。 [[[[[[[self requestAccessToTwitterSignal] then:^RACSignal *{ @strongify(self) return self.searchText.rac_textSignal; }] filter:^BOOL(NSString *text) { @strongify(self) return [self isValidSearchText:text]; }] throttle:0.5] flattenMap:^RACStream *(NSString *text) { @strongify(self) //flattenMap来将每个next事件映射到一个新的被订阅的信号 return [self signalForSearchWithText:text]; }] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDictionary *jsonSearchResult) { NSArray *statuses = jsonSearchResult[@"statuses"]; NSArray *tweets = [statuses linq_select:^id(id tweet) { return [RWTweet tweetWithStatus:tweet]; }]; [self.resultsViewController displayTweets:tweets]; } error:^(NSError *error) { NSLog(@"An error occurred: %@", error); }]; - (RACSignal *)signalForSearchWithText:(NSString *)text { // 1 - define the errors NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorNoTwitterAccounts userInfo:nil]; NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain code:RWTwitterInstantErrorInvalidResponse userInfo:nil]; @weakify(self) return [RACSignal createSignal:^RACDisposable *(id subscriber) { @strongify(self); SLRequest *request = [self requestforTwitterSearchWithText:text]; NSArray *twitterAccounts = [self.accountStore accountsWithAccountType:self.twitterAccountType]; if (twitterAccounts.count == 0) { [subscriber sendError:noAccountsError]; } else { [request setAccount:[twitterAccounts lastObject]]; [request performRequestWithHandler: ^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) { if (urlResponse.statusCode == 200) { NSDictionary *timelineData = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingAllowFragments error:nil]; [subscriber sendNext:timelineData]; [subscriber sendCompleted]; } else { [subscriber sendError:invalidResponseError]; } }]; } return nil; }]; }
-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl { RACScheduler *scheduler = [RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground]; return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]]; UIImage *image = [UIImage imageWithData:data]; [subscriber sendNext:image]; [subscriber sendCompleted]; return nil; }] subscribeOn:scheduler]; } cell.twitterAvatarView.image = nil; [[[self signalForLoadingImage:tweet.profileImageUrl] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(UIImage *image) { cell.twitterAvatarView.image = image; }];
@weakify(self); [[[RACSignal merge: @[RACObserve(self.viewModel, tweets), RACObserve(self.viewModel, allTweetsLoaded)]] bufferWithTime: 0 onScheduler: [RACScheduler mainThreadScheduler]] subscribeNext: ^(id value) { @strongify(self); [self.tableView reloadData]; }]; //bufferWithTime设置为0是为了避免同一时刻两个值被同时设置新值产生了table进行了两次reloadData
@weakify(self); [[tableView rac_signalForSelector:@selector(layoutSubviews)]subscribeNext:^(id x) { @strongify(self); [self doSomethingBeforeTableViewLayoutSubviews]; }];
Demo的github地址: https://github.com/olegam/RACCommandExample
- (void)bindWithViewModel { RAC(self.viewModel, email) =self.emailTextField.rac_textSignal; self.subscribeButton.rac_command = self.viewModel.subscribeCommand; RAC(self.statusLabel, text) =RACObserve(self.viewModel, statusMessage); } @interface SubscribeViewModel :NSObject @property(nonatomic, strong)RACCommand *subscribeCommand; // writeto this property @property(nonatomic, strong) NSString *email; // read from this property @property(nonatomic, strong) NSString *statusMessage; @end #import "SubscribeViewModel.h" #import "AFHTTPRequestOperationManager+RACSupport.h" #import"NSString+EmailAdditions.h" static NSString *const kSubscribeURL =@"http://reactivetest.apiary.io/subscribers"; @interface SubscribeViewModel () @property(nonatomic, strong) RACSignal*emailValidSignal; @end @implementation SubscribeViewModel - (id)init { self= [super init]; if(self) { [self mapSubscribeCommandStateToStatusMessage]; } returnself; } -(void)mapSubscribeCommandStateToStatusMessage { RACSignal *startedMessageSource = [self.subscribeCommand.executionSignals map:^id(RACSignal *subscribeSignal) { return NSLocalizedString(@"Sending request...", nil); }]; RACSignal *completedMessageSource = [self.subscribeCommand.executionSignals flattenMap:^RACStream *(RACSignal *subscribeSignal) { return[[[subscribeSignal materialize] filter:^BOOL(RACEvent *event) { return event.eventType == RACEventTypeCompleted; }] map:^id(id value) { return NSLocalizedString(@"Thanks", nil); }]; }]; RACSignal*failedMessageSource = [[self.subscribeCommand.errors subscribeOn:[RACSchedulermainThreadScheduler]] map:^id(NSError *error) { return NSLocalizedString(@"Error :(", nil); }]; RAC(self,statusMessage) = [RACSignal merge:@[startedMessageSource, completedMessageSource, failedMessageSource]]; } - (RACCommand *)subscribeCommand { if(!_subscribeCommand) { @weakify(self); _subscribeCommand = [[RACCommand alloc] initWithEnabled:self.emailValidSignal signalBlock:^RACSignal *(id input) { @strongify(self); return [SubscribeViewModel postEmail:self.email]; }]; } return _subscribeCommand; } + (RACSignal *)postEmail:(NSString *)email{ AFHTTPRequestOperationManager*manager = [AFHTTPRequestOperationManager manager]; manager.requestSerializer= [AFJSONRequestSerializer new]; NSDictionary*body = @{@"email": email ?: @""}; return [[[manager rac_POST:kSubscribeURL parameters:body] logError] replayLazily]; } - (RACSignal *)emailValidSignal { if(!_emailValidSignal) { _emailValidSignal= [RACObserve(self, email) map:^id(NSString *email) { return@([email isValidEmail]); }]; } return _emailValidSignal; } @end
RAC会维护一个全局的信号集合,一个或多于一个订阅者就可用,所有订阅者都被移除了,信号就被释放了。
RACCommand RAC中用于处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程。
RACMulticastConnection 用于当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的block,造成副作用,可以使用这个类处理。