美文网首页MVVM模式
RAC 双向绑定实现案例

RAC 双向绑定实现案例

作者: BenXia | 来源:发表于2017-06-15 19:48 被阅读1228次

    案例1:正常情况下实现两个属性双向绑定


    方法一:

    RACChannelTo(view, property) = RACChannelTo(model, property);
    

    方法二:(与方法一完全等价)

    [[RACKVOChannel alloc] initWithTarget:view keyPath:@"property" nilValue:nil][@"followingTerminal"] 
    = [[RACKVOChannel alloc] initWithTarget:model keyPath:@"property" nilValue:nil][@"followingTerminal"];
    

    方法三:(中间需要做一些映射转换的)

    RACChannelTerminal *channelA = RACChannelTo(self, valueA);
    RACChannelTerminal *channelB = RACChannelTo(self, valueB);
    
    // valueA: On表示打开,Off表示关闭
    // valueB: 1表示打开,0表示关闭
    
    [[channelA map:^id(NSString *value) {
        if ([value isEqualToString:@"On"]) {
            return @"1";
        } else {
            return @"0";
        }
    }] subscribe:channelB];
    
    [[channelB map:^id(NSString *value) {
        if ([value isEqualToString:@"1"]) {
            return @"On";
        } else {
            return @"Off";
        }
    }] subscribe:channelA];
    

    案例2:实现UISwitch跟随NSUserDefaults存储的值变化


    方法1:

    [[RACKVOChannel alloc] initWithTarget:[NSUserDefaults standardUserDefaults]
                                      keyPath:@"someBoolKey" nilValue:@(NO)][@"followingTerminal"] = [[RACKVOChannel alloc] initWithTarget:self.someSwitch keyPath:@"on" nilValue:@(NO)][@"followingTerminal"];
        
    // 上面的不能完全实现双向绑定,因为 UISwitch 的 on 属性是不支持 KVO 的                        
    @weakify(self)
    [self.someSwitch.rac_newOnChannel subscribeNext:^(NSNumber *onValue) {
        @strongify(self)
        
        // 下面两句都可以
        [self.someSwitch setValue:onValue forKey:@"on"];
        //[[NSUserDefaults standardUserDefaults] setObject:onValue forKey:@"someBoolKey"];
    }];
    

    方法2:代码摘自stackoverflow上一个问题答案

    // 注意下面代码实现是不能满足的
    RACChannelTerminal *switchTerminal = self.someSwitch.rac_newOnChannel;
    RACChannelTerminal *defaultsTerminal = [[NSUserDefaults standardUserDefaults] rac_channelTerminalForKey:@"someBoolKey"];
    [switchTerminal subscribe:defaultsTerminal];
    [defaultsTerminal subscribe:switchTerminal];
    

    但是我自己创建了一个工程发现这个双向绑定有问题,我点击两次UISwitch后,再用代码修改NSUserDefaults中对应值,结果UISwitch没有变化。

    经过调试发现原因是因为当操作UISwitch控件时,触发defaultsTerminal,但是RAC的NSUserDefaults+RACSupport的rac_channelTerminalForKey实现中filter操作会过滤,导致后面的distinctUntilChanged操作中的__block变量lastValue没有更新,这样下次再修改NSUserDefaults中的相应值时,distinctUntilChanged对比的已经是上上次的lastValue,导致defaultsTerminal没有触发,从而没有触发switchTerminal,从而导致双向绑定失败。

    我暂时的解决方法是新建一个NSUserDefaults+CustomRACSupport的category方法,将原先实现中的"filter"操作去掉,因为"distinctUntilChanged"已经能做"filter"操作想做的事情。

    去掉"filter"操作后的方法实现如下:

    - (RACChannelTerminal *)customChannelTerminalForKey:(NSString *)key {
        RACChannel *channel = [RACChannel new];
        
        RACScheduler *scheduler = [RACScheduler scheduler];
        
        @weakify(self);
        [[[[[[NSNotificationCenter.defaultCenter
            rac_addObserverForName:NSUserDefaultsDidChangeNotification object:self]
            map:^(id _) {
                @strongify(self);
                return [self objectForKey:key];
            }]
            startWith:[self objectForKey:key]]
            distinctUntilChanged]
            takeUntil:self.rac_willDeallocSignal]
            subscribe:channel.leadingTerminal];
        
        [[channel.leadingTerminal
            deliverOn:scheduler]
            subscribeNext:^(id value) {
                @strongify(self);
                [self setObject:value forKey:key];
            }];
        
        return channel.followingTerminal;
    }
    

    案例3:UITextField/UITextView与viewModel中的text属性双向绑定


    如果将textView的数据绑定写成下面这样

    RACChannelTo(self, uiTextView.text) = RACChannelTo(self, viewModel.text);
    

    你会发现viewModel.text不会随着键盘输入的内容改变而发生变化。但是用代码修改viewModel.text的值时代码改变的值却能同步到uiTextView上面。

    具体原因可以查看stackoverflow上一个相似的issue

    其实官方文档是这么说的:UIKit classes don't expose KVO-compliant properties UIKIt里面的很多控件本身不支持KVO,而ReactiveCocoa本身是基于KVO实现的,所以就会出现这种双向绑定不成功的现象,这时候就需要我们手动用信号,或者是rac提供的其他属性来做处理完成双向绑定的操作

    另外注意下 self.uiTextField.rac_newTextChannel 与 RACChannelTo(self.uiTextField, text) 的区别
    同样的 self.uiTextView/uiTextField.rac_textSignal 与 RACObserve(self.uiTextView/uiTextField, text)也有该区别

    self.uiTextField.rac_newTextChannel sends values when you type in the text field, but not when you change the text in the text field from code.

    RACChannelTo(self.uiTextField, text) sends values when you change the text in the text field from code, but not when you type in the text field.

    所以代码写成下面这样也是有漏洞的:

    RACChannelTerminal *textFieldChannelT = uiTextField.rac_newTextChannel;
    RAC(self.viewModel, text) = textFieldChannelT;
    [RACObserve(self.viewModel, text) subscribe:textFieldChannelT];
    // 当用代码给uiTextField.text赋值时会影响不到self.viewModel.text
    

    顺便提一个自己曾经遇到的坑:
    当订阅self.uiTextView.rac_textSignal后,原先uiTextView设置的delegate相关委托方法会不回调。(UITextField没有这个问题,具体原因可以看下ReactiveCocoa的UITextView的rac_textSignal的实现)

    解决方法:

    由于使用代码对model到view这个方向的绑定是没问题的,所以我们只要在textView的text改变的信号中做一个手动的设置值(在subscribeNext中主动设置model对应的属性值就可以完成双向绑定了)

    代码如下:

    #import "ViewController.h"
    #import <ReactiveCocoa/ReactiveCocoa.h>
     
    @interface Model : NSObject
    
    @property (nonatomic, strong) NSString *text;
    
    @end
    
    @implementation Model
    
    @end
    
    
    @interface ViewController ()
    
    @property (nonatomic, strong) UITextView *textView;
    @property (nonatomic, strong) Model *model;
    @property (nonatomic, copy) NSString *str;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 80, CGRectGetWidth(self.view.bounds), 300)];
        self.textView.backgroundColor = [UIColor redColor];
        [self.view addSubview:self.textView];
    
        self.model = [Model new];
    
        // 这种写法其实已经是双向绑定的写法了,但是由于是textView的原因只能绑定model.text的变化到影响textView.text的值的变化的这个单向通道
        RACChannelTo(self,textView.text) = RACChannelTo(self,model.text);
    
        // 在这里对textView的text changed的信号重新订阅一下,以实现上面channel未实现的另外一个绑定通道.
       @weakify(self)
       [self.textView.rac_textSignal subscribeNext:^(id x) {
           @strongify(self)
    
           self.model.text = x;
           NSLog(@"model text is%@",self.model.text);
       }];
    
       UIButton *resetBtn = [[UIButton alloc] initWithFrame:CGRectMake(0, 480, 60, 40)];
       resetBtn.backgroundColor = [UIColor yellowColor];
       [resetBtn setTitle:@"reset" forState:UIControlStateNormal];
       [self.view addSubview:resetBtn];
    
       resetBtn.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            @strongify(self)
            RACSignal *signal = [RACSignal return:input];
            [signal subscribeNext:^(id x) {
                self.model.text = @"reset yet";
                NSLog(@"model text is%@",self.model.text);
            }];
    
            return signal;
        }];
    }
    
    @end
    

    还有两个有趣的案例 详见链接

    本文代码详见:https://github.com/BenXia/RACTwoWayBinding

    相关文章

      网友评论

        本文标题:RAC 双向绑定实现案例

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