RAC 双向绑定实现案例

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容