美文网首页从0到1学习RAC
iOS ReactiveCocoa学习笔记(4):进阶用法

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

作者: 一粒咸瓜子 | 来源:发表于2019-06-14 15:32 被阅读5次

参考资料:《神奇的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 坑点

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.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; 
}]; 

相关文章

网友评论

    本文标题:iOS ReactiveCocoa学习笔记(4):进阶用法

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