iOS开发基础111-RAC
ReactiveCocoa(RAC)是一个基于函数响应式编程(FRP)的框架,广泛用于iOS开发中。其核心思想是通过流和信号(signal)来处理多变、复杂的事件。以下是ReactiveCocoa常见的一些用法场景,并深入解析其原理。
1. 响应用户输入
场景:表单验证
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
RACSignal *validUsernameSignal = [self.usernameTextField.rac_textSignal
map:^id _Nullable(NSString * _Nullable username) {
return @(username.length > 3);
}];
RACSignal *validPasswordSignal = [self.passwordTextField.rac_textSignal
map:^id _Nullable(NSString * _Nullable password) {
return @(password.length > 3);
}];
RACSignal *formValidSignal = [RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
reduce:^id (NSNumber *usernameValid, NSNumber *passwordValid){
return @([usernameValid boolValue] && [passwordValid boolValue]);
}];
[formValidSignal subscribeNext:^(NSNumber *formValid) {
self.loginButton.enabled = [formValid boolValue];
}];
原理解析:
rac_textSignal
将UITextField的文字变化转化为信号。map:
操作符将输入的字符串转化为有效性的布尔值(长度大于3)。combineLatest:reduce:
将多个信号结合成一个新的信号,并通过reduce
块生成最终结果。subscribeNext:
订阅信号,且在信号值变化时对登录按钮进行启用/禁用。
2. 数据绑定
场景:双向绑定ViewModel和View
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
RAC(self.viewModel, username) = self.usernameTextField.rac_textSignal;
RAC(self.usernameTextField, text) = RACObserve(self.viewModel, username);
原理解析:
RAC(target, keyPath)
将信号绑定到ViewModel的属性。RACObserve
创建一个观察信号,当viewModel.username
变化时,更新UITextField的text。- 双向绑定确保View和ViewModel的属性同步变化。
3. 处理异步操作
场景:网络请求
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
- (RACSignal *)fetchDataFromURL:(NSURL *)url {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
[subscriber sendError:error];
} else {
[subscriber sendNext:data];
[subscriber sendCompleted];
}
}];
[task resume];
return [RACDisposable disposableWithBlock:^{
[task cancel];
}];
}];
}
[[self fetchDataFromURL:url] subscribeNext:^(NSData *data) {
// Process data
} error:^(NSError *error) {
// Handle error
}];
原理解析:
createSignal:
创建一个信号并手动控制其数据(如下一步、完成、错误)流的发送。- 在信号内部,执行异步操作(例如网络请求),并在操作完成时发送事件(数据、完成、错误)。
subscribeNext:error:
对信号的成功和失败情况进行订阅和处理。
4. 信号组合和变换
场景:信号合并和过滤
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
// 合并信号
RACSignal *mergedSignal = [RACSignal merge:@[self.signalA, self.signalB]];
[mergedSignal subscribeNext:^(id _Nullable x) {
NSLog(@"Received: %@", x);
}];
// 过滤信号
RACSignal *filteredSignal = [self.inputSignal filter:^BOOL(id _Nullable value) {
return [value length] > 3;
}];
[filteredSignal subscribeNext:^(id _Nullable x) {
NSLog(@"Filtered value: %@", x);
}];
原理解析:
merge:
将多个信号合并成一个信号,同时订阅多个信号的所有事件。filter:
只允许满足条件的事件通过,其他事件将被过滤掉。
5. 基于时间的操作
场景:延迟、定时器、节流
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
// 延迟操作
[[RACSignal return:@"Hello, World!"] delay:2.0];
// 定时器
RACSignal *timerSignal = [RACSignal interval:1.0 onScheduler:[RACScheduler mainThreadScheduler]];
[timerSignal subscribeNext:^(NSDate * _Nullable x) {
NSLog(@"Timer tick: %@", x);
}];
// 节流操作
RACSignal *throttledSignal = [[self.usernameTextField.rac_textSignal throttle:0.5];
[throttledSignal subscribeNext:^(NSString * _Nullable x) {
NSLog(@"Throttled value: %@", x);
}];
原理解析:
delay:
将信号的所有事件延迟指定的时间发送。interval:
创建一个定时器信号,每隔指定的时间发出一个事件。throttle:
在指定的时间内只发送最新的一次事件,防止高频率触发。
6. 错误处理
场景:网络请求错误与重试机制
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
- (RACSignal *)fetchDataFromURL:(NSURL *)url {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
[subscriber sendError:error];
} else {
[subscriber sendNext:data];
[subscriber sendCompleted];
}
}];
[task resume];
return [RACDisposable disposableWithBlock:^{
[task cancel];
}];
}];
}
[[[self fetchDataFromURL:url] retry:3] subscribeNext:^(NSData *data) {
// Process data
} error:^(NSError *error) {
// Handle error
}];
原理解析:
retry:
在信号遇到错误时重新订阅信号,最多重试指定的次数。- 使用
sendError:
,sendNext:
,sendCompleted:
手动控制信号的错误和数据发送。
7. 依赖信号
场景:顺序执行多个异步任务
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
- (RACSignal *)loginSignal:(NSString *)username password:(NSString *)password {
// 登录 signal implementation
}
- (RACSignal *)fetchUserProfileSignal {
// 获取用户Profile的信号
}
[[[self loginSignal:@"user" password:@"pass"]
flattenMap:^__kindof RACSignal * _Nullable(id _Nullable value) {
return [self fetchUserProfileSignal];
}]
subscribeNext:^(id _Nullable x) {
NSLog(@"User Profile: %@", x);
} error:^(NSError * _Nullable error) {
NSLog(@"Error: %@", error);
}];
原理解析:
flattenMap:
将第一个信号的值映射为另一个信号,并订阅这个新信号。这通常用于依赖关系,即在一个任务完成后启动另一个任务。- 在登录成功后,使用
flattenMap:
触发获取用户Profile的信号。
8. 控件事件处理
场景:按钮点击事件
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIButton *myButton;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[self.myButton rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(UIButton *button) {
NSLog(@"Button clicked");
}];
}
@end
原理解析:
rac_signalForControlEvents:
将UI控件的事件(例如按钮的点击)转化为信号。- 通过
subscribeNext:
订阅该信号,以处理点击事件。
9. 定时操作
场景:倒计时功能
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UILabel *countdownLabel;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__block int remainingTime = 60;
RACSignal *countdownSignal = [[RACSignal interval:1.0 onScheduler:[RACScheduler mainThreadScheduler]]
take:remainingTime];
[countdownSignal subscribeNext:^(NSDate * _Nullable x) {
self.countdownLabel.text = [NSString stringWithFormat:@"%d", remainingTime];
remainingTime--;
} completed:^{
self.countdownLabel.text = @"Time's up!";
}];
}
@end
原理解析:
interval:onScheduler:
创建一个每秒发出事件的信号。take:
指定信号发送的事件次数。- 通过
subscribeNext:
更新界面。
10. 表格视图刷新
场景:下拉刷新
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
#import <MJRefresh/MJRefresh.h>
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
[[self fetchData] subscribeNext:^(NSArray *data) {
// Update table view with new data
[self.tableView reloadData];
[self.tableView.mj_header endRefreshing];
}];
}];
}
- (RACSignal *)fetchData {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
// Simulate network request
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSArray *data = @[@"Item 1", @"Item 2", @"Item 3"];
[subscriber sendNext:data];
[subscriber sendCompleted];
});
return nil;
}];
}
@end
原理解析:
- 使用第三方库MJRefresh添加下拉刷新控件。
- 在刷新时,发起数据请求的信号并更新表格视图数据。
- 在订阅数据之后结束刷新状态。
11. 链式反应
场景:密码和确认密码验证
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
RACSignal *validPasswordSignal = [self.passwordTextField.rac_textSignal
map:^id _Nullable(NSString * _Nullable password) {
return @(password.length > 3);
}];
RACSignal *validConfirmPasswordSignal = [RACSignal
combineLatest:@[self.passwordTextField.rac_textSignal, self.confirmPasswordTextField.rac_textSignal]
reduce:^id (NSString *password, NSString *confirmPassword) {
return @(password.length > 3 && [password isEqualToString:confirmPassword]);
}];
[[RACSignal combineLatest:@[validPasswordSignal, validConfirmPasswordSignal]
reduce:^id (NSNumber *passwordValid, NSNumber *confirmPasswordValid){
return @([passwordValid boolValue] && [confirmPasswordValid boolValue]);
}]
subscribeNext:^(NSNumber *formValid) {
self.registerButton.enabled = [formValid boolValue];
}];
原理解析:
rac_textSignal
处理密码和确认密码输入框的文本变化。combineLatest:reduce:
将两个信号结合,并在两个信号联动时验证密码和确认密码。- 通过组合后的信号来控制注册按钮是否可用。
12. 进度条更新
场景:文件下载进度
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
- (RACSignal *)downloadFileWithProgress {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url = [NSURL URLWithString:@"https://example.com/file.zip"];
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url
completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
[subscriber sendError:error];
} else {
[subscriber sendCompleted];
}
}];
[task resume];
return [RACDisposable disposableWithBlock:^{
[task cancel];
}];
}];
}
[[self downloadFileWithProgress] subscribeCompleted:^{
self.progressView.progress = 1.0;
} error:^(NSError * _Nullable error) {
NSLog(@"Download failed: %@", error);
}];
[RACSignal interval:0.1 onScheduler:[RACScheduler mainThreadScheduler]]
subscribeNext:^(NSDate * _Nullable x) {
self.progressView.progress = (arc4random_uniform(100) / 100.0);
}];
原理解析:
createSignal:
进行文件下载,并在下载完成时发送完成事件。- 创建一个定时信号来模拟下载进度,并实时更新UI。
13. 监听值变化
场景:属性绑定
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
@interface MyViewModel : NSObject
@property (nonatomic, strong) NSString *name;
@end
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@property (nonatomic, strong) MyViewModel *viewModel;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.viewModel = [[MyViewModel alloc] init];
RAC(self.nameLabel, text) = RACObserve(self.viewModel, name);
self.viewModel.name = @"Initial name";
}
@end
原理解析:
RACObserve
监听ViewModel属性变化。- 使用
RAC(target, keyPath)
绑定属性值到UILabel的text属性。
14. 动态计算属性
场景:计算两数之和
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UITextField *firstNumberTextField;
@property (weak, nonatomic) IBOutlet UITextField *secondNumberTextField;
@property (weak, nonatomic) IBOutlet UILabel *sumLabel;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
RACSignal *firstNumberSignal = [self.firstNumberTextField.rac_textSignal map:^id _Nullable(NSString * _Nullable text) {
return @([text integerValue]);
}];
RACSignal *secondNumberSignal = [self.secondNumberTextField.rac_textSignal map:^id _Nullable(NSString * _Nullable text) {
return @([text integerValue]);
}];
RACSignal *sumSignal = [RACSignal combineLatest:@[firstNumberSignal, secondNumberSignal] reduce:^id (NSNumber *firstNumber, NSNumber *secondNumber) {
return @([firstNumber integerValue] + [secondNumber integerValue]);
}];
RAC(self.sumLabel, text) = [sumSignal map:^id _Nullable(NSNumber * _Nullable sum) {
return [sum stringValue];
}];
}
@end
原理解析:
map:
将文本转化为数字。combineLatest:reduce:
计算两数之和。- 使用
RAC(target, keyPath)
绑定结果到UILabel。
15. 处理通知
场景:监听键盘弹出和隐藏
示例代码:
#import <ReactiveObjC/ReactiveObjC.h>
@interface ViewController ()
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
RACSignal *keyboardShowSignal = [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillShowNotification object:nil] takeUntil:self.rac_willDeallocSignal];
RACSignal *keyboardHideSignal = [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillHideNotification object:nil] takeUntil:self.rac_willDeallocSignal];
RACSignal *keyboardFrameSignal = [RACSignal merge:@[keyboardShowSignal, keyboardHideSignal]]
.map(^id (NSNotification *notification) {
return [notification.userInfo valueForKey:UIKeyboardFrameEndUserInfoKey];
}];
[keyboardFrameSignal subscribeNext:^(NSValue *keyboardFrame) {
CGRect frame = keyboardFrame.CGRectValue;
self.bottomConstraint.constant = frame.size.height;
[UIView animateWithDuration:0.25 animations:^{
[self.view layoutIfNeeded];
}];
}];
}
@end
原理解析:
rac_addObserverForName:
监听键盘显示和隐藏通知。map:
将通知转化为键盘的frame值。- 通过订阅信号动态调整底部约束。
这些场景展示了ReactiveCocoa在iOS开发中的多种应用,通过信号和操作符,能够简化异步操作和事件处理,使代码更加简洁和可维护。
将来的你会感谢今天如此努力的你!
版权声明:本文为博主原创文章,未经博主允许不得转载。