RAC中双向监听回声问题

前言

公司产品最近要实现一个需求,大概如下:
1.一个UITextFidleUISwitch联动

2.UITextFidle在编辑状态下,UISwitch处于开启状态

3.UISwitch处于开启状态时,UITextFidle相应的处于可编辑状态

RAC中双向监听回声问题_第1张图片

  1. UITextFidle退出编辑状态时,输入框未输入任何字符,UISwitch处于关闭状态

  2. UISwitch处于关闭状态时,UITextFidle退出编辑状态

    RAC中双向监听回声问题_第2张图片

  3. UITextFidle退出编辑状态时,输入框有字符,UISwitch处于开启状态

    RAC中双向监听回声问题_第3张图片

看到这样的需求,第一时间想到利用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中双向监听回声问题_第4张图片

代码很简单,RAC给UITextFidle添加了分类方法- (RACChannelTerminal *)rac_newTextChannel;,可以很简单的去生成RACChannelTerminal,去实现双向绑定。同样也给UITextViewUISwitchUIStepperUISliderUISegmentedControlUIControlUIDatePicker控件添加了相应的分类方法去生成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.分别将UIControlEventEditingDidBeginUIControlEventEditingDidEnd事件产生的信号mapReplace成1和0,然后merge成一个UITextField编辑状态改变的信号;将该信号转换成热信号。
2.将UISwitch开闭状态改变的信号装换成热信号。
3.将上诉两个热信号先调取ignoreValues,这是去除回声问题的关键,忽略的所有信号中的值,使得自己无法监听到自己值得变化,打破了闭环。
4.分别调用subscribe:方法,使另一个信号成为自己的订阅者。使得对方信号发送时,自己可以监听到对方的改变。
5.分别订阅1和2中产生的信号,当其中有一个控件状态改变时,改变另一个控件的状态。这样就可以实现上面的需求了。

说明

文章中使用的RAC为2.5.0版本。

你可能感兴趣的:(RAC中双向监听回声问题)