打算在项目中大面积使用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,造成副作用,可以使用这个类处理。