前言
公司产品最近要实现一个需求,大概如下:
1.一个UITextFidle
和UISwitch
联动
2.UITextFidle
在编辑状态下,UISwitch
处于开启状态
3.UISwitch
处于开启状态时,UITextFidle
相应的处于可编辑状态
-
UITextFidle
退出编辑状态时,输入框未输入任何字符,UISwitch
处于关闭状态 -
UISwitch
处于关闭状态时,UITextFidle
退出编辑状态 -
UITextFidle
退出编辑状态时,输入框有字符,UISwitch
处于开启状态
看到这样的需求,第一时间想到利用RAC监听属性变化去实现。
回声问题
回声问题指的是,当相互监听属性时,不仅对方可以监听到自己属性的变化,自己也可以监听到自己的变化。这样就陷入了一个死循环,两边都能听到对方的变化,还能同时听到自己的变化。
例如这样去实现:
RACSignal *editingDidBeginSignal = [[textField rac_signalForControlEvents:UIControlEventEditingDidBegin] mapReplace:@1];
RACSignal *editingDidEndSignal = [[textField rac_signalForControlEvents:UIControlEventEditingDidEnd] map:^id(UITextField *value) {
return value.text.length ? @1 : @0;
}];
RACSignal *switchSignal = [[valueSwitch rac_signalForControlEvents:UIControlEventTouchUpInside] map:^NSNumber *(UISwitch *value) {
return @(value.on);
}];
[[editingDidEndSignal merge:editingDidBeginSignal] subscribeNext:^(NSNumber *x) {
valueSwitch.on = [x boolValue];
}];
[switchSignal subscribeNext:^(NSNumber *x) {
x.boolValue ? [textField becomeFirstResponder] : [textField resignFirstResponder];
}];
这就会产生典型的回声问题,当UITextFidle
编辑状态改变时,会改变UISwitch
的开闭状态;而UISwitch
的开闭状态改变时,UITextFidle
又会监听到变化改变编辑状态,从而进入了无限的循环之中。
RACChannel
RAC中是有实现双向绑定的成熟方案的,这就是RACChannel与RACChannelTerminal。例如两个UITextFidle
,任意一个UITextFidle
输入文本变化时,另一个也要跟着变化
代码很简单,RAC给UITextFidle
添加了分类方法- (RACChannelTerminal *)rac_newTextChannel;
,可以很简单的去生成RACChannelTerminal,去实现双向绑定。同样也给UITextView
,UISwitch
,UIStepper
,UISlider
,UISegmentedControl
,UIControl
,UIDatePicker
控件添加了相应的分类方法去生成RACChannelTerminal。
上图中的代码也很简单:
[textField1.rac_newTextChannel subscribe:textField2.rac_newTextChannel];
[textField2.rac_newTextChannel subscribe:textField1.rac_newTextChannel];
什么是```RACChannel和RACChannelTerminal呢?
RACChannel
可以看成是一个双向通道,由两个并行工作的可控信号组成。而RACChannelTerminal
则是这个双向通道的一端。可以简单理解为,两个属性双向监听,相当于在这两个属性直接建立个一个通道,而其中的一端就是RACChannelTerminal
。类比于网络编程里面socket的概念,RACChannel
类似网络链接通道,RACChannelTerminal
类似于socket。
具体实现可以参考这篇文章。
关于上面的需求
本来我是想利用RACChannelTerminal
去实现的,可以看到UITextField
的分类实现
- (RACChannelTerminal *)rac_newTextChannel {
return [self rac_channelForControlEvents:UIControlEventAllEditingEvents key:@keypath(self.text) nilValue:@""];
}
- (RACChannelTerminal *)rac_channelForControlEvents:(UIControlEvents)controlEvents key:(NSString *)key nilValue:(id)nilValue {
NSCParameterAssert(key.length > 0);
key = [key copy];
RACChannel *channel = [[RACChannel alloc] init];
[self.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
[channel.followingTerminal sendCompleted];
}]];
RACSignal *eventSignal = [[[self
rac_signalForControlEvents:controlEvents]
mapReplace:key]
takeUntil:[[channel.followingTerminal
ignoreValues]
catchTo:RACSignal.empty]];
[[self
rac_liftSelector:@selector(valueForKey:) withSignals:eventSignal, nil]
subscribe:channel.followingTerminal];
RACSignal *valuesSignal = [channel.followingTerminal
map:^(id value) {
return value ?: nilValue;
}];
[self rac_liftSelector:@selector(setValue:forKey:) withSignals:valuesSignal, [RACSignal return:key], nil];
return channel.leadingTerminal;
}
是监听text属性的变化,想仿照此写法但是发现UITextField
成为第一响应者和退出第一响应者,没法找到具体的属性,这里也就没法利用RACChannelTerminal
去实现了。
转化下思路,既然没法用现成的方案实现,可以参考RACChannel
的实现思路,自己去实现双向绑定。
RACSignal *editingDidBeginSignal = [[textField rac_signalForControlEvents:UIControlEventEditingDidBegin] mapReplace:@1];
RACSignal *editingDidEndSignal = [[textField rac_signalForControlEvents:UIControlEventEditingDidEnd] map:^id(UITextField *value) {
return value.text.length ? @1 : @0;
}];
//转化成热信号
RACSubject *textEditingSignal = (RACSubject *)[[editingDidBeginSignal merge:editingDidEndSignal] replay];
RACSubject *switchSignal = (RACSubject *)[[[valueSwitch rac_signalForControlEvents:UIControlEventTouchUpInside] map:^NSNumber *(UISwitch *value) {
return @(value.on);
}] replay];
//ignoreValues避免自己可以监听到自己的变化,处理回声问题的关键
//subscribe:方法使后者成为前者的其中之一的订阅者
[[textEditingSignal ignoreValues] subscribe:switchSignal];
[[switchSignal ignoreValues] subscribe:textEditingSignal];
//订阅UITextField和UISwitch相应的信号
[textEditingSignal subscribeNext:^(id x) {
valueSwitch.on = [x boolValue];
}];
[switchSignal subscribeNext:^(id x) {
if ([x boolValue]) {
//选中
[textField becomeFirstResponder];
} else {
//未选中
textField.text = @"";
[textField resignFirstResponder];
}
}];
具体思路:
1.分别将UIControlEventEditingDidBegin
和UIControlEventEditingDidEnd
事件产生的信号mapReplace
成1和0,然后merge
成一个UITextField
编辑状态改变的信号;将该信号转换成热信号。
2.将UISwitch
开闭状态改变的信号装换成热信号。
3.将上诉两个热信号先调取ignoreValues
,这是去除回声问题的关键,忽略的所有信号中的值,使得自己无法监听到自己值得变化,打破了闭环。
4.分别调用subscribe:
方法,使另一个信号成为自己的订阅者。使得对方信号发送时,自己可以监听到对方的改变。
5.分别订阅1和2中产生的信号,当其中有一个控件状态改变时,改变另一个控件的状态。这样就可以实现上面的需求了。
说明
文章中使用的RAC为2.5.0版本。
网友评论