RAC 双向绑定实现案例

271 篇文章 0 订阅
20 篇文章 2 订阅

案例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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值