iOS ReactiveCocoa学习笔记(4):进阶用法


参考资料:《神奇的RAC宏》

本文知识点:RACMulticastConnection(避免多次请求)、RACCommand(网络请求,监听命令状态,区别与RACSubject)、RAC常用宏。

1. RACMulticastConnection

  • 当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的block,造成副作用,可以使用这个类处理.
  • 注意:RACMulticastConnection通过RACSignal-publish或者-muticast:方法创建。

1.1 使用步骤

  1. 创建信号 + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
  2. 创建连接 RACMulticastConnection *connect = [signal publish];
  3. 订阅信号,注意:订阅的不再是之前的信号,而是connect的信号。 [connect.signal subscribeNext:nextBlock]
  4. 连接 [connect connect]

1.2 底层原理

  1. 创建connectconnect.sourceSignal -> RACSignal(原始信号) connect.signal -> RACSubject
  2. 订阅connect.signal,会调用RACSubject的subscribeNext,创建订阅者,而且把订阅者保存起来,不会执行block
  3. [connect connect]内部会订阅RACSignal(原始信号),并且订阅者是RACSubject
    1> 订阅原始信号,就会调用原始信号中的didSubscribe
    2> didSubscribe,拿到订阅者调用sendNext,其实是调用RACSubject的sendNext
  4. RACSubjectsendNext,会遍历RACSubject所有订阅者发送信号。
    1> 因为刚刚第二步,都是在订阅RACSubject,因此会拿到第二步所有的订阅者,调用他们的nextBlock

1.3 应用场景:避免多次请求数据

需求:假设在一个信号中发送请求,每次订阅一次都会发送请求,这样就会导致多次请求。
解决:使用RACMulticastConnection就能解决。

// 使用RACSignal出现的情况:多次执行nextBlock
RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
    static int i = 0;
    i++;
    [subscriber sendNext:@(i)];
    return nil;
}];

[signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"x = %@", x);
}];
[signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"x = %@", x);
}];
[signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"x = %@", x);
}];

Output:
2019-06-14 15:24:32.321216+0800 RacDemo[7221:2430572] x = 1
2019-06-14 15:24:32.321341+0800 RacDemo[7221:2430572] x = 2
2019-06-14 15:24:32.321483+0800 RacDemo[7221:2430572] x = 3


// 解决重复请求问题使用RACMulticastConnection处理:
RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
    static int i = 0;
    i++;
    [subscriber sendNext:@(i)];
    return nil;
}];
//创建连接
RACMulticastConnection *connect = [signal publish];
//订阅信号
//注意:订阅信号,也不能激活信号,只是保存订阅者到数组,必须通过连接,当调用连接,就会一次性调用所有订阅者的sendNext:
[connect.signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"x = %@", x);
}];
[connect.signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"x = %@", x);
}];
[connect.signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"x = %@", x);
}];
//连接,激活信号(须写在最后)
[connect connect];

Output:
2019-06-14 15:28:44.720282+0800 RacDemo[7224:2431304] x = 1
2019-06-14 15:28:44.720343+0800 RacDemo[7224:2431304] x = 1
2019-06-14 15:28:44.720361+0800 RacDemo[7224:2431304] x = 1

2. RACCommand

  • RAC中用于处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程。
  • 监听按钮点击,网络请求。

2.1 使用步骤

  1. 创建命令 initWithSignalBlock:(RACSignal * (^)(id input))signalBlock
  2. signalBlock中,创建RACSignal,并且作为signalBlock的返回值
  3. 执行命令 - (RACSignal *)execute:(id)input

使用注意:

  1. signalBlock必须要返回一个信号,不能传nil.
  2. 如果不想要传递信号,直接创建空的信号[RACSignal empty];
  3. RACCommand中信号如果数据传递完,必须调用[subscriber sendCompleted],这时命令才会执行完毕,否则永远处于执行中。
    4.buttonrac_commandenbaleSignal不能共存。需要使用带有enable参数的方法初始化command

2.2 设计思想

RACCommand设计思想:内部signalBlock为什么要返回一个信号,这个信号有什么用。

  1. 在RAC开发中,通常会把网络请求封装到RACCommand,直接执行某个RACCommand就能发送请求。
  2. RACCommand内部请求到数据的时候,需要把请求的数据传递给外界,这时候就需要通过signalBlock返回的信号传递了。

如何拿到RACCommand中返回信号发出的数据:

  1. RACCommand有个执行信号源executionSignals,这个是signal of signals(信号的信号),意思是信号发出的数据是信号,不是普通的类型。
  2. 订阅executionSignals就能拿到RACCommand中返回的信号,然后订阅signalBlock返回的信号,就能获取发出的值。

2.3 应用场景:监听网络请求

// 一般采用懒加载的方式
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
    //signalBlock必须要返回一个信号,不能传nil.
    //如果不想要传递信号,直接创建空的信号[RACSignal empty];
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        //信号的创建是在主线程中{number = 1, name = main}
        NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
        [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            //NSURLSession自动切入子线程申请网络请求{number = 5, name = (null)}
            if (error) {
                [subscriber sendError:error];
            } else {
                NSString *dataString = [[NSString alloc] initWithData:data encoding:kCFStringEncodingUTF8];
                [subscriber sendNext:dataString];
                //RACCommand中信号如果数据传递完,必须调用sendCompleted,这时命令才会执行完毕,否则永远处于执行中。
                [subscriber sendCompleted];
            }
        }] resume];
        return nil;
    }];
}];

//强引用命令,不要被销毁,否则接收不到数据
_command = command;

//command executionSignals是信号的信号,订阅它可以拿到signalblock返回的信号
[[_command executionSignals] subscribeNext:^(id  _Nullable x) {
    //外层信号是在主线程中创建的,所以订阅信号也是在主线程中{number = 1, name = main}
    NSLog(@"%@" ,x);
    [x subscribeNext:^(id  _Nullable x) {
        //因为最内层信号是在子线程中发的,所以也是在子线程中订阅到这个信号的{number = 5, name = (null)}
        NSLog(@"%@", x);
    }];
}];

//或者 RAC的高级用法 switchToLatest:
//switchToLatest:用于signal of signals,获取signal of signals发出的最新信号,也就是可以直接拿到RACCommand中的信号
[_command.executionSignals.switchToLatest subscribeNext:^(id  _Nullable x) {
    //因为最内层信号是在子线程中发的,所以也是在子线程中订阅到这个信号的{number = 5, name = (null)}
    NSLog(@"%@",x);
}];

//要先订阅信号,后面再执行命令才能收到返回的数据 !!!
[_command execute:@1];

信号的监听:
监听当前命令是否正在执行:

//监听命令是否执行完毕,默认会来一次,可以直接跳过,skip表示跳过第一次信号。
[[_command.executing skip:1] subscribeNext:^(NSNumber * _Nullable x) {
    if (x.boolValue) {
        NSLog(@"正在执行");
    } else {
        NSLog(@"执行完毕");
    }
}];
Output:
正在执行
…数据…
执行完毕

2.4 坑点

2.4.1 rac_command与enable不可同时存在

button.rac_command已赋值,那么就不能再将button.enbaleSignal绑定:

// 不能同时存在
btn.rac_command = self.viewModel.loginCommand;
RAC(btn, enabled) = self.viewModel.enableSignal;

解决办法:使用带有Enable参数的api初始化RACComand

- (instancetype)initWithEnabled:(nullable RACSignal<NSNumber *> *)enabledSignal signalBlock:(RACSignal<ValueType> * (^)(InputType _Nullable input))signalBlock;

btn.rac_command = [[RACCommand alloc] initWithEnabled:self.enableSignal signalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
        return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
            [subscriber sendNext:nil];
            [subscriber sendCompleted];
            return nil;
        }];
    }];

2.4.2 网络错误的获取

error的获取并不是使用 command.executionSignals.switchToLatest subscribeError:来获取的。可以用两种方法来获取:

  1. 订阅command.errors
[_command.executionSignals.switchToLatest subscribeNext:^(id  _Nullable x) {
    // success
}];
[_command.errors subscribeNext:^(NSError * _Nullable x) {
    // error 
}];
  1. 订阅[command execute:]
@weakify(self);
[[Btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    @strongify(self);
    [[self.viewModel.command execute:nil] subscribeNext:^(id  _Nullable x) {
        // success    
    } error:^(NSError * _Nullable error) {
        // error    
    }];
}];

2.5 与RACSubject区别

RACSubject较为灵活,建议少用,通常用来代替delegate
区别用计算机网络中的术语来讲:RACSubject更像“单工”,而RACCommand就类似于“半双工”。

RACSubject只能单向发送事件,发送者将事件发送出去让接收者接收事件后进行处理,所以,RACSubject可代替代理,被监听者可利用subject发送事件,监听者接收事件然后进行相应的监听处理,不过,事件的传递方向是单向的。

对于RACCommand,用HTTP请求能够更形象地说明其原理,HTTP请求是由请求者向服务器发送一条网络请求,而服务器接收到请求然后经过相应处理后再向请求者返回处理过后的结果,数据流是双向的,RACCommand正是如此。当我想让某个部件进行某种会产生结果的操作时,利用RACCommand向此部件发送执行事件,部件接收到执行事件后进行相应操作处理并也通过RACCommand将操作结果回调到上层,使得事件得以双向流通。
以上的解释是建立在RACCommand的事件产生与接收者为同一个对象的前提下的,而RACCommand也能将事件产生者和订阅者分离,让某个对象专门发送事件,通过RACCommand将事件传递到对数据进行操作处理的对象,最后,当数据处理完后再搭载着RACCommand把结果事件传出来,并被订阅者对象订阅。

下面的这张图表明了我对RACSubject与RACCommand的理解:



3. 常见宏

3.1 RAC()

RAC(TARGET, [KEYPATH, [NIL_VALUE]])

  • 这个宏是最常用的,RAC()总是出现在等号左边,等号右边是一个RACSignal,表示的意义是将一个对象的一个属性和一个signal绑定。
  • signal每产生一个value(id类型),都会自动执行 [TARGET setValue:value ?: NIL_VALUE forKeyPath:KEYPATH]
  • 数字值会升级为NSNumber *,当setValue:forKeyPath时会自动降级成基本类型(int, float ,BOOL等)所以RAC绑定一个基本类型的值是没有问题的。宏定义如下:
#define RAC(TARGET, ...) \
    metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__)) \
        (RAC_(TARGET, __VA_ARGS__, nil)) \
        (RAC_(TARGET, __VA_ARGS__))

应用场景:


只要文本框文字改变,就会修改 label 的文字
//Ex:只要文本框文字改变,就会修改 label 的文字
RAC(_label, text) = _textField.rac_textSignal;

//Ex:监听两个文本框的内容,有内容才允许按钮点击
RAC(_login_btn, enabled) = [RACSignal combineLatest:@[_usernama_tf.rac_textSignal, _pwd_tf.rac_textSignal] reduce:^id _Nonnull (NSString *userName, NSString *pwd){
    return @(userName.length > 0 && pwd.length > 0);
}];

3.2 RACObeserve()

RACObserve(TARGET, KEYPATH)

  • 作用是观察TARGET的KEYPATH属性,相当于KVO,产生一个RACSignal。最常用的使用,和RAC宏绑定属性。宏定义如下:
#define _RACObserve(TARGET, KEYPATH) \
({ \
    __weak id target_ = (TARGET); \
    [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \
})
#if __clang__ && (__clang_major__ >= 8)
#define RACObserve(TARGET, KEYPATH) _RACObserve(TARGET, KEYPATH)
#else
#define RACObserve(TARGET, KEYPATH) \
({ \
    _Pragma("clang diagnostic push") \
    _Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \
    _RACObserve(TARGET, KEYPATH) \
    _Pragma("clang diagnostic pop") \
})
#endif

应用场景:双向绑定

1> Model -> UI  RAC(TARGET, ...) = RACObserve(TARGET, KEYPATH);
//如果使用基本数据类型绑定UI内容,需要使用map函数,通过block对value的数值进行转换后才能够绑定。
RAC(name_textField, text) = RACObserve(_person, name);
//rac中传递的数据都是id类型,如果是基本类型,需要使用map函数,通过block对value的数值进行转换后才能够绑定。
RAC(age_textField, text) = [RACObserve(_person, age) map:^id _Nullable(id  _Nullable value) {
    return [value description];
}];
2> UI -> Model
@weakify(self);
[[RACSignal combineLatest:@[[name_tf rac_textSignal], [age_tf rac_textSignal]]] subscribeNext:^(RACTuple * _Nullable x) {
    @strongify(self);
    self.person.name = x.first;
    self.person.age = [x.second integerValue];
}];

3.3 RACTuplePack

把数据包装成RACTuple。宏定义如下:

#define RACTuplePack(...) \
    RACTuplePack_(__VA_ARGS__)
#define RACTuplePack_(...) \
    ([RACTuplePack_class_name(__VA_ARGS__) tupleWithObjectsFromArray:@[ metamacro_foreach(RACTuplePack_object_or_ractuplenil,, __VA_ARGS__) ]])

应用场景:

RACTuple *tuple = RACTuplePack(@1, @2);

3.4 RACTupleUnpack

RACTuple(元组类)解包成对应的数据。按顺序给对象赋值:如果对象数量多于元祖内对象数,后面不赋值。宏定义如下:

#define RACTupleUnpack(...) \
        RACTupleUnpack_(__VA_ARGS__)
#define RACTupleUnpack_(...) \
    metamacro_foreach(RACTupleUnpack_decl,, __VA_ARGS__) \
    \
    int RACTupleUnpack_state = 0; \
    \
    RACTupleUnpack_after: \
        ; \
        metamacro_foreach(RACTupleUnpack_assign,, __VA_ARGS__) \
        if (RACTupleUnpack_state != 0) RACTupleUnpack_state = 2; \
        \
        while (RACTupleUnpack_state != 2) \
            if (RACTupleUnpack_state == 1) { \
                goto RACTupleUnpack_after; \
            } else \
                for (; RACTupleUnpack_state != 1; RACTupleUnpack_state = 1) \
                    [RACTupleUnpackingTrampoline trampoline][ @[ metamacro_foreach(RACTupleUnpack_value,, __VA_ARGS__) ] ]

应用场景:

RACTuple *tuple = RACTuplePack(@1, @2);
RACTupleUnpack(NSString *title1, NSString *title2, NSString *title3) = tuple;
NSLog(@"%@, %@, %@", title1, title2, title3);

Output:
2018-04-05 14:33:58.859491+0800 RACDemo[12628:374427] 1, 2, (null)

3.5 weakify、strongify

@weakify(obj)、@strongify(obj)

一般配套使用,解决引用循环。因为系统提供的信号是始终存在的,因此在RAC中所有的block中,如果出现self/成员变量,几乎百分之百会循环引用。
应用场景:

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

推荐阅读更多精彩内容