美文网首页
RAC中双向监听回声问题

RAC中双向监听回声问题

作者: NSBug | 来源:发表于2018-08-09 22:45 被阅读96次

    前言

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

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

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

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

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

    3. 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,去实现双向绑定。同样也给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中双向监听回声问题

          本文链接:https://www.haomeiwen.com/subject/tgfhbftx.html