最近在学习RAC框架,断断续续的看了好久,看的文章博客可以说少说也得有一两百,所以这是一个综合的产物,也可以算是一个学习笔记,毕竟好记性不如烂笔头,记录下来,以供以后复习。如有侵权,联系立删。
书归正传:
RAC简介
RAC是一个F(functional)R(reactive)P(programming)框架,可以很好的结合MVVM. 写出的项目易于维护。但是导入MVVM和RAC的项目,学习成本有点高,如果自己独立开发,或者团队内成员水平较平均且普遍较高,那么可以尝试这么做。
注意:
RAC框架已经有几年的历史了,理所当然的有OC,Swift版本,从2.5版本之后,就开始全面支持swift,不再支持OC
,所以如果你的项目是OC的项目,请使用cocoapods导入2.5或之前的版本
,如果你对cocoapods不甚了解,请移步我的cocoapods踩坑:
另外需要了解的:
MVVM设计模式。
相信大家对MVC肯定都很熟悉,Model业务模型,View前端显示,Controller控制器。
但是一般我们在开发的时候,(仅针对iOS开发)M层一般只会写有几个成员属性,也就是说,所有的业务逻辑,网络层代码之类的,全部都写在了Controller中,这样,就造成了C层中有大量的,冗杂的代码,让人维护起来很费劲,本着‘高内聚,低耦合’的原则,有些大神们就想到了MVVM的设计模式。
本人理解
如有不对,还请各位大神批评指正。
MVVM其实是M-V-C-VM,也就是比正常的MVC多了一个ViewModel,
VM从网络请求数据,然后参照Model的格式和一些需求,处理数据,然后返回给Controller,再传递给View展示出来
,大概就是图中的画出来的样子
ViewModel主要从事的业务
1. 从网络请求、处理数据,然后返回给Controller,controller再传递给View层显示
2. 界面中的业务逻辑
也就是把以前Controller中的逻辑代码,换了个地方,全部写在了ViewModel中了。
所以,从根本上来说,MVC和MVVM是可以相互转换的。
下面从使用中来了解RAC。
重要的类
RACSignal
这个是RAC中最重要的一个类了(RAC中全部都是以信号来处理的)
信号分为热信号和冷信号,默认创建完成之后是冷信号,这个时候是无用的,也就是值改变了,也不会触发,只有订阅subscribe
了之后,信号才能变成热信号,订阅的block(subscribeNext
)才会被调用。
创建信号的方法:
+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe;
可以看到返回了一个block,该block返回值为RACDisposable,参数为subscriber,该参数遵守RACSubscriber协议
- RACDisposable,如果想要取消订阅,使用此类的方法
- (void)dispose;
- RACSubscriber协议中的方法有:
- (void)sendNext:(id)value;
- (void)sendError:(NSError *)error;
- (void)sendCompleted;
用来传值,或者返回数据。
订阅信号的方法
- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
该方法是RACSignal
subscription类别中的一个方法。
调用之后,冷信号变成热信号,nextBlock
会执行
使用中要注意:
1. 在block中,如果订阅之后,不想传值,可以使用协议中的方法
[subscriber sendNext:nil]
,若想传,则在该方法后传入参数(必须id类)
2. 传值完成之后,必须调用sendCompleted
方法,以示订阅完成,否则,无法再次订阅信号。
3. 如果返回值为ERROR,且要传递,调用sendError
方法,sendNext
方法无法传递错误信息
了解了信号和订阅,我们就可以开始简单的使用一下了。
假如界面中有一个textview一个label. 我们可以绑定他们两个,以达到用户在textview中输入的值,显示在label中
//界面的布局之类的不再写,只写下逻辑块代码
[self.textview.rac_textSignal subscribeNext:^(id x) {
self.label.text = (NSString *)x;
}];
代码中的rac_textSignal返回的是textView的文本信号。我们订阅之后,就可以获得这个信号,其中的id x
就是textview中的文本,所以我们直接赋值给label.text
就OK了
RACCommand
这个类,是我花了好久时间才看懂的一个类。主要从事事件的处理。
比如网络请求这种,一方传递出去一个信号,需要另一方返回一个信号的这种流程。前后端通信这种就是一个很好的例子: 前端post数据给后端,然后服务器返回给我们相应的JSON字符串,这个就可以使用RACCommand来处理。
与RACSignal的区别, RACSignal是单向的,只能后者订阅前者,反之则不行
创建方法
- (id)initWithSignalBlock:(RACSignal * (^)(id input))signalBlock;
该方法只有一个 block参数,创建的时候,要在block中创建一个RACSignal,并返回,如果不想返回信号,则可以return [RACSignal empty];
来返回一个空的信号。- (id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock;
该方法除了一个 block参数之外,还有一个enable信号,用来控制command能否执行。
执行方法
- (RACSignal *)execute:(id)input;
只有执行了,订阅的block才会调用
属性
- RACCommand有几个属性
- executionSignals
- executing
- errors
这些属性的类型都是RACSignal. 目的就是在执行之后,可以订阅,查看信号内的内容。 - allowsConcurrentExecution BOOL类型的变量,是否允许同时多个执行,默认是NO
注意:
一般情况下,[self.command.executionSignals subscribeNext:^(id x) {...}]
订阅的时候,里边的x,是RACSignal
类的实例,我们此时订阅这个信号,就可以看到我们发出的信号(外层信号)的内容,如果想要看到里边包装的信号,也就是信号中的信号,我们可以使用
- 双重订阅
[self.command.executionSignals subscribeNext:^(RACSignal *x) {
[x subscribeNext:^(id x){
NSLog(@"%@",x);
}];
}]
- RAC的高级用法
// switchToLatest 获取信号中信号最近发出信号,订阅最近发出的信号。
[[self.command.executionSignals switchToLatest]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
switchToLatest方法: 获取信号中信号最近发出信号,订阅最近发出的信号。也就是可以跳过外层的信号,直接订阅内部的信号。
或者
//跳过第一次订阅,直接订阅第二次
[[self.command.executionSignals skip:1] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
RACCommand的用法
//1,先创建
RACCommand *cmd = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"传值"];
[subscriber sendCompleted];
return nil;
}];
}];
//2. 订阅
//信号中的信号,要双层订阅
[cmd.executionSignals subscribeNext:^(id x) {
NSLog(@"%@",x); //<RACDynamicSignal: 0x79e99050> name:
[x subscribeNext:^(id inner) {
NSLog(@"%@",inner); //传值
}];
}];
//开始订阅的时候会打Log
[cmd.executing subscribeNext:^(id x) {
NSLog(@"executing");
}];
//如果出错了,会走这里
[cmd.errors subscribeNext:^(id x) {
NSLog(@"error");
NSLog(@"%@",x);
}];
//3. 执行
[cmd execute:@"执行"];
RACSubject
介绍
RACSubject:信号提供者,自己可以充当信号,又能发送信号。
使用步骤
- 创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
- 订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
- 发送信号 sendNext:(id)value
一般用途
替换代理,block
//在第二个VC里边声明一个RACSubject属性,以反向传值为例,第二个页面的背景色为随机色,点击按钮POP回第一个页面,然后把颜色传给第一个页面
@interface SecViewController : UIViewController
@property (nonatomic,strong) RACSubject *delegateSubject;
@end
@implementation SecViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *btn = [[UIButton alloc]init];
[btn setTitle:@"点击传值" forState:UIControlStateNormal];
btn.frame = CGRectMake(0, 0, 100, 44);
btn.center = self.view.center;
[[btn rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
if (self.delegateSubject) {
[self.delegateSubject sendNext:self.view.backgroundColor];
[self.delegateSubject sendCompleted];
}
[self.navigationController popViewControllerAnimated:YES];
}];
[self.view addSubview:btn];
}
//第一个页面的PUSH代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
SecViewController *sec = [[SecViewController alloc]init];
sec.delegateSubject = [RACSubject subject];
[sec.delegateSubject subscribeNext:^(id x) {
NSLog(@"%@",x);
self.view.backgroundColor = (UIColor *)x;
}];
sec.hidesBottomBarWhenPushed = YES;
[self.navigationController pushViewController:sec animated:YES];
}
可以在代码段里看到一个[btn rac_signalForControlEvents:UIControlEventTouchUpInside]
,这个方法会返回按钮的command中的信号,系统默认为我们做的,我们只要订阅这个信号,就可以执行按钮对应的Action了
RACTuple
RAC中的元组类,用来包装值,类似NSArray
创建方法
+ (instancetype)tupleWithObjectsFromArray:(NSArray *)array;
+ (instancetype)tupleWithObjectsFromArray:(NSArray *)array convertNullsToNils:(BOOL)convert;
+ (instancetype)tupleWithObjects:(id)object, ... NS_REQUIRES_NIL_TERMINATION;
属性
@property (nonatomic, readonly) NSUInteger count;
/// These properties all return the object at that index or nil if the number of
/// objects is less than the index.
@property (nonatomic, readonly) id first;
@property (nonatomic, readonly) id second;
@property (nonatomic, readonly) id third;
@property (nonatomic, readonly) id fourth;
@property (nonatomic, readonly) id fifth;
@property (nonatomic, readonly) id last;
所以我们可以使用索引来获取Tuple中的值
NSArray *testArr = @[@"1",@"2",@"3",@"4"];
RACTuple *tuple = [RACTuple tupleWithObjectsFromArray:testArr];
NSLog(@"%@",tuple[0]); //1
NSLog(@"%@",tuple[1]); //2
NSLog(@"%@",tuple.third); //3
如果遇到NSDictionary,则RACTuple则会循环键值对的个数次,倒叙的遍历字典的键值对(通常是把一个键值对当成数组的一个元素)
NSDictionary *testDic = @{@"name":@"testData",@"age":@(20)};
[testDic.rac_sequence.signal subscribeNext:^(RACTuple *x) {
NSLog(@"%@",x);
NSLog(@"%ld",(long)x.count);
NSLog(@"%@",x[0]);
NSLog(@"%@",x[1]);
NSLog(@"%@",x.third);
}];
循环 第一次,log结果:(<RACTuple: 0x79e49550> (age,20) 2 age 20 (null))
第二次结果:(<RACTuple: 0x7c183bc0> (name,testData) 2 name testData (null))
我们可以使用RACTupleUnpack把tuple转换成dictionary的键值关系
RACTupleUnpack(NSString *key, NSString *value) = x;
上面的写法和下面的得到的结果是一样的
NSString *key = x[0];
NSString *value = x[1];
RACSequence
简介
RAC中快速遍历Array和Dictionary的类
用途
- 遍历数组字典
NSArray *testArr = @[@"1",@"2",@"3",@"4"];
[testArr.rac_sequence.signal subscribeNext:^(id x) {
NSLog(@"%@",x); //<RACTuple: 0x796386c0> ( number,1 )
}];
NSDictionary *testDic = @{@"name":@"testData",@"age":@(20)};
[testDic.rac_sequence.signal subscribeNext:^(id x) {
NSLog(@"%@",x);
#if 0
RACTupleUnpack(NSString *key, NSString *value) = x;
NSLog(@"%@,%@",key,value);
#else
NSString *key = x[0];
NSString *value = x[1];
NSLog(@"%@,%@",key,value);
#endif
}];
- 字典转模型
//OC写法
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"flags.plist" ofType:nil];
NSArray *dictArr = [NSArray arrayWithContentsOfFile:filePath];
NSMutableArray *items = [NSMutableArray array];
for (NSDictionary *dict in dictArr) {
FlagItem *item = [FlagItem flagWithDict:dict];
[items addObject:item];
}
//RAC写法
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"flags.plist" ofType:nil];
NSArray *dictArr = [NSArray arrayWithContentsOfFile:filePath];
NSMutableArray *flags = [NSMutableArray array];
_flags = flags;
// rac_sequence注意点:调用subscribeNext,并不会马上执行nextBlock,而是会等一会。
[dictArr.rac_sequence.signal subscribeNext:^(id x) {
// 运用RAC遍历字典,x:字典
FlagItem *item = [FlagItem flagWithDict:x];
[flags addObject:item];
}];
NSLog(@"%@", NSStringFromCGRect([UIScreen mainScreen].bounds));
//RAC高级写法:
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"flags.plist" ofType:nil];
NSArray *dictArr = [NSArray arrayWithContentsOfFile:filePath];
// map:映射的意思,目的:把原始值value映射成一个新值
// array: 把集合转换成数组
// 底层实现:当信号被订阅,会遍历集合中的原始值,映射成新值,并且保存到新的数组里。
NSArray *flags = [[dictArr.rac_sequence map:^id(id value) {
return [FlagItem flagWithDict:value];
}] array];
rac_signalForSelector
简介
可以代替代理
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"Message" message:@"You will push to the second ViewController,right?" delegate:self cancelButtonTitle:@"cacel" otherButtonTitles:@"okay", nil];
#pragma clang diagnostic pop
[alert show];
[[self rac_signalForSelector:@selector(alertView:clickedButtonAtIndex:) fromProtocol:@protocol(UIAlertViewDelegate)] subscribeNext:^(RACTuple *x) {
NSLog(@"%@",x);
if ([x[1] boolValue]) {
[self pushToSecViewController];
}
}];
//AlertView的简化写法
[[alertView rac_buttonClickedSignal] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
self.textField.delegate = self;
[[self rac_signalForSelector:@selector(textFieldDidBeginEditing:) fromProtocol:@protocol(UITextFieldDelegate)] subscribeNext:^(id x) {
NSLog(@"开始编辑");
}];
[[self rac_signalForSelector:@selector(textFieldDidEndEditing:) fromProtocol:@protocol(UITextFieldDelegate)]
subscribeNext:^(id x) {
NSLog(@"结束编辑");
}];
跟RACSubject区别
RACSubject可以传值
rac_signalForSelector: fromProtocol: 不可以传值
代替KVO
rac_valuesAndChangesForKeyPath
// 把监听view的center属性改变转换成信号,只要值改变就会发送信号
// observer:可以传入nil
[[view rac_valuesAndChangesForKeyPath:@"center" options:NSKeyValueObservingOptionNew observer:nil] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
代替通知
rac_addObserverForName
// 把监听到的通知转换信号
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillShowNotification object:nil] subscribeNext:^(id x) {
NSLog(@"键盘弹出");
}];
处理当界面有多次请求时,需要都获取到数据才能进行下一步
rac_liftSelector:withSignalsFromArray:Signals
当传入的Signals(信号数组),每一个signal都至少sendNext过一次,就会去触发第一个selector参数的方法
//处理多个请求,都返回结果的时候,统一做处理.
RACSignal *signal1 = [RACSignal createSignal:^RACDisposable *(id<racsubscriber> subscriber) {
// 发送请求1
[subscriber sendNext:@"发送请求1"];
return nil;
}];
RACSignal *signal2 = [RACSignal createSignal:^RACDisposable *(id<racsubscriber> subscriber) {
// 发送请求2
[subscriber sendNext:@"发送请求2"];
return nil;
}];
// Selector调用:当所有信号都发送数据的时候调用
// 数组存放信号
// Selector注意点:参数根据数组元素决定
// Selector方法参数类型,就是信号传递出来数据
[self rac_liftSelector:@selector(doingSthWithData1: data2:) withSignalsFromArray:@[signal1,signal2]];
RAC常见宏
RAC(target, keyPath)
用法
用于给某个对象的某个属性绑定。例子
// 只要文本框文字改变,就会修改label的文字
RAC(self.labelView,text) = _textField.rac_textSignal;
RACOberve(self, name)
- 用法
监听某个对象的某个属性,返回的是信号。 - 例子
[RACObserve(self.view, center) subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
先写到这里,回头会有补充
本文demo地址
另外还有一个使用RAC+MVVM模拟的登录小demo
参考资料:
学习RAC小记-适合给新手看的RAC用法总结
ReactiveCocoa(FRP)-进阶篇
被误解的 MVC 和被神化的 MVVM
ReactiveCocoa干货