美文网首页
iOS ReactiveCocoa(RAC)番外篇

iOS ReactiveCocoa(RAC)番外篇

作者: 小白进城 | 来源:发表于2019-09-29 10:14 被阅读0次

在上一篇ReactiveCocoa(RAC)教程中,我们学习了ReactiveCocoa的一些基础内容。本篇中我们继续介绍一些有趣的用法。

NSObject相关的信号

在基础教程中我们知道,ReactiveCocoa使用分类扩展一套标准UI库的信号,如文本框的rac_textSignal、按钮的rac_signalForControlEvents:等,也有通过多个信号生成组合信号。除此之外,ReactiveCocoa同样对NSObject类进行了多种扩展,其中包括类似KVO形式的实现,属性监听信号,方法调用信号。当然,这些信号是基于createSignal:来创建的。

KVO扩展

NSObject+RACKVOWrapper.h文件中,ReactiveCocoa 实现了对实例属性KVO的实现。话不多说,我们先来体验一下。

首先需要在文件头部导入该分类:

#import <ReactiveCocoa/NSObject+RACKVOWrapper.h>

原因是ReactiveCocoa其实优化了属性监听,有更好的方式去监听对象属性,但是实现是基于该文件的,所以这里还是介绍一下。

创建一个名为Person的类:

#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (strong, nonatomic) NSString* name;
@end

@implementation Person
@end

在控制器中导入类Person.h,然后创建一个实例并初始化:

@property (strong, nonatomic)Person* p;

- (void)viewDidLoad {
    [super viewDidLoad];
    self.p = [Person new];
}

现在我们来监听实例pname属性变化,在文件viewDidLoad方法中继续添加一下代码:

[self.p rac_observeKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld observer:nil block:^(id value, NSDictionary *change) {
    NSLog(@"value:%@",value);
    NSLog(@"NSDictionary:%@",change);
}];
// 修改值,触发监听回调
self.p.name = @"Joy";

运行应用程序,控制台会输出:

value:Joy
NSDictionary:{
    RACKeyValueChangeAffectedOnlyLastComponentKey = 1;
    RACKeyValueChangeCausedByDeallocationKey = 0;
    kind = 1;
    new = Joy;
    old = "<null>";
}

酷~这么简单?就是这么简单!

在经典的KVO设计模式中,我们需要为被观察者添加观察对象addObserver:forKeyPath:options:context:,并且需要实现其代理方法observeValueForKeyPath:ofObject:change:context:,还需要注意循环引用和释放问题,如果需要监听的对象过多,还需要分门别类的进行区分等等,代理的形式的缺陷就是不够直观,而ReactiveCocoa通过回调的方式让整个业务更直观。

观察没有必要明确移除,在观察者或者接收者释放时将被删除

属性监听

为了将KVO统一,让其符合ReactiveCocoa的信号流特点,通常我们并不会直接采用1.1中的形式,这也就是为什么ReactiveCocoa将其放在头文件中。ReactiveCocoa对NSObject的还有其他方法,可以用来创建RACSignal用于管道的构建。

[[self.p rac_valuesAndChangesForKeyPath:@"name" options:NSKeyValueObservingOptionNew observer:nil]
 subscribeNext:^(id x) {
     NSLog(@"x:%@",x);
 }];
self.p.name = @"Joy";

运行项目,控制台输出:

x:<RACTuple: 0x600003828550> (
    Joy,
        {
        RACKeyValueChangeAffectedOnlyLastComponentKey = 1;
        RACKeyValueChangeCausedByDeallocationKey = 0;
        kind = 1;
        new = Joy;
    }
)

发现输出的内容是RACTuple类型的内容,它实际上是ReactiveCocoa自己实现的元组类型(OC中没有元组),类似Swift、python等语言中元组。

我们修改一下代码,使用RACTuple来接收数据:

[[self.p rac_valuesAndChangesForKeyPath:@"name" options:NSKeyValueObservingOptionNew observer:nil]
 subscribeNext:^(RACTuple* x) {
     NSLog(@"value:%@ \n dic:%@",x.first, x.second);
 }];
self.p.name = @"Joy";

运行项目,控制台输出:

value:Joy 
 dic:{
    RACKeyValueChangeAffectedOnlyLastComponentKey = 1;
    RACKeyValueChangeCausedByDeallocationKey = 0;
    kind = 1;
    new = Joy;
}

我们看到,输出结果和1.1中的结果一致,但是上述的例子中是一个最简单的管道结构,通过信号将属性的变化流向其订阅者。也你可以将该信号添加到已存在的其他管道中,实现更复杂的数据流。

优化RACObserve

在属性监听时,我们通常只关心新值,其他状态的值并不关心,没关系,ReactiveCocoa也同样考虑到了这一点,它还有一个更简化的方法:

[[self.p rac_valuesForKeyPath:@"name" observer:nil]
 subscribeNext:^(id x) {
     NSLog(@"%@",x);
 }];
self.p.name = @"Joy";

运行项目,控制台输出:

(null)
Joy

可以看到,使用了这个简化方法后,数据流传过来的最终值x只有新值,但是敏锐的你可能要问:为什么只修改一次,订阅块却被执行了两次?

我们点开该方法的实现部分:

- (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath observer:(NSObject *)observer {
    return [[[self rac_valuesAndChangesForKeyPath:keyPath options:NSKeyValueObservingOptionInitial observer:observer] reduceEach:^(id value, NSDictionary *change) {
        return value;
    }] setNameWithFormat:@"RACObserve(%@, %@)", self.rac_description, keyPath];
}

我们看到该方法在调用rac_valuesAndChangesForKeyPath:options:observer:时,options给的是NSKeyValueObservingOptionInitial,该选项使得初始化的时候的改变也同样会触发回调。

不过没有关系,ReactiveCocoa提供了一个方法,用来跳过指定次数的回调skip:,我们来修改一下代码:

[[[self.p rac_valuesForKeyPath:@"name" observer:nil] skip:1]
 subscribeNext:^(id x) {
     NSLog(@"%@",x);
 }];
self.p.name = @"Joy";

skip:n表示跳过第n次的next事件,其他照旧。

运行项目,控制台输出:

Joy

很好,我们得到了最想要的东西。似乎没什么可以说的了,不!ReactiveCocoa 并不满足于此,ReactiveCocoa中有非常多的宏,用来快速使用一些功能,没错,上述代码通样有自己的宏:

[[RACObserve(self.p, name) skip:1]
 subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];
self.p.name = @"Joy";

运行项目,控制台会输出同样的结果。上述代码中的RACObserve是你在项目经常使用到的。

注意到RACObserve宏,

#define RACObserve(TARGET, KEYPATH) \
    [(id)(TARGET) rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]

它接收一个对象,和其keypath,因此上述代码同样可以修改为:

[[RACObserve(self, p.name) skip:1]
 subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];
self.p.name = @"Joy";

方法监听

以为做到KVO转换就满足了吗?不!ReactiveCocoa为了将数据流的概念深入推进,还做到了将方法转换为信号,将方法调用进行了监听。

我们给Person添加一个方法,用于后面演示。

@interface Person : NSObject
@property (strong, nonatomic) NSString* name;
-(void)say:(NSString*)some;
@end

@implementation Person
-(void)say:(NSString *)some{
    NSLog(@"%@",some);
}
@end

viewDidLoad中继续添加:

[[self.p rac_signalForSelector:@selector(say:)]
 subscribeNext:^(id x) {
     NSLog(@"%@",x);
 }];
// 某个时刻
[self.p say:@"hello world!"];

运行项目,控制台输出:

hello world!
<RACTuple: 0x600000ed0500> (
    "hello world!"
)

项目先输出了方法say:方法的结果hello world!,在我们订阅块中,得到了一个元组类型,其中包括了方法say:方法的参数。

操作事件监听

针对UIControl类,ReactiveCocoa也进行了扩展,让我们能够快速的得到各种事件的回调。以我们常用的按钮控件来演示:

// 按钮的点击事件
[[self.myButton rac_signalForControlEvents:UIControlEventTouchUpInside]
 subscribeNext:^(id x) {
     // do something ...
 }];

非RAC和RAC的桥梁 : RACSubject

RACSubjectRACSignal的子类,并且遵循了协议RACSubscriber,这让你可以手动控制发送nextcompletederror事件,让非RAC事件转为RAC事件。使用RACSubject可以让您替代回调块、代理等,使用起来更像是回调块,可作为参数、属性、返回值来使用,但是和回调块的一对一不同,RACSubject可以添加个订阅块,当发送next事件时,每个订阅块都可以收到数据流。

// 创建一个RACSubject对象
RACSubject* subject = [RACSubject subject];
// 添加订阅1
[subject subscribeNext:^(id x) {
    NSLog(@"1:%@",x);
}];
// 添加订阅2
[subject subscribeNext:^(id x) {
    NSLog(@"2:%@",x);
}];
// 完成事件
[subject subscribeCompleted:^{
    NSLog(@"完成");
}];
// 某个时刻
[subject sendNext:@[@"data1",@"data2"]];
[subject sendCompleted];

运行项目,控制台输出:

1:(
    data1,
    data2
)
2:(
    data1,
    data2
)
完成

我们看到在subject发送了一个next事件,两个订阅者都收到了相同的数据。

在只有提前订阅的块才能收到RACSubject发出的next事件。另外RACSubject是单向的,它无法收到订阅块的消息,如果你需要发送具有反馈的消息,你可能需要使用到RACCommand

双向事件 RACCommand

RACCommandRACSubject不同,RACSubject是单向传递事件,而当你发送一则消息并且需要反馈时,RACSubject通常不行,但是你可以使用RACCommand来实现双向消息。

// command的创建需要一个信号,这个信号用来反向传递数据
RACCommand* command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
    NSLog(@"%@",input);
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        // 模拟网络请求
        [RACScheduler.currentScheduler afterDelay:1.0 schedule:^{
            [RACScheduler.mainThreadScheduler schedule:^{
                [subscriber sendNext:@"RACSignal信号发出消息。"];
                [subscriber sendCompleted];
            }];
        }];
        return nil;
    }];
}];

// 添加订阅,由于command传递过来的是创建时的那个信号,所以我们使用信号进行订阅
[command.executionSignals subscribeNext:^(RACSignal* x) {
    // 订阅
    [x subscribeNext:^(id x) {
        NSLog(@"%@",x);
    }];
}];

//    // 转换为最新的信号
//    [command.executionSignals.switchToLatest subscribeNext:^(id x) {
//        NSLog(@"%@",x);
//    }];

// command传递信息
[command execute:@"RACCommand传递过来的信息"];

运行项目,控制台输出:

RACCommand传递过来的信息
RACSignal信号发出消息。

RACCommand的创建需要你提供是一个RACSignal信号,这个信号用来反馈数据,如网络请求、本地数据库读写等等,另外,block中的参数inputRACCommand执行时传入的数据,即你正向传递的数据(相对内部的信号)。

在创建完成之后,我们使用RACCommand的属性executionSignals进行订阅,因为你提供的是信号,所以数据流过来的还是信号,因此需要你使用信号接收并再次进行订阅,当然你也可以使用switchToLatest转换信号中的信号,直接获取最终数据。

最后,在某个合适的时机使用execute:方法执行RACCommand

如果你想要在RACCommand执行时做一些等待提示操作,并在执行后取消提示,你则可以这样:

[command.executionSignals subscribeNext:^(RACSignal* x) {
    // 开启提示
    [x subscribeNext:^(id x) {
        // 结束提示
        //  Do something...
    }];
}];

errors 错误事件

如果RACCommad内部信号发出error事件,你无法通过以下代码获取到错误。

首先修改内部信号,发出error事件:

// command的创建需要一个信号,这个信号用来反向传递数据
RACCommand* command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
    NSLog(@"%@",input);
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        // 模拟网络请求
        [RACScheduler.currentScheduler afterDelay:1.0 schedule:^{
            [RACScheduler.mainThreadScheduler schedule:^{
//                    [subscriber sendNext:@"RACSignal信号发出消息。"];
//                    [subscriber sendCompleted];
                [subscriber sendError:[[NSError alloc] initWithDomain:@"error" code:-100 userInfo:@{@"value":@"发生错误。"}]];
            }];
        }];
        return nil;
    }];
}];

尝试订阅错误:

[command.executionSignals.switchToLatest subscribeNext:^(id x) {
    NSLog(@"%@",x);
} error:^(NSError *error) {
    NSLog(@"%@",error);
}];

运行应用程序,你会发现没有收到任何消息。

正确的方式是使用RACCommand中的另一个属性errors,它也是一个信号类型,用来传递发生错误信息。

// RACCommand的错误事件
[command.errors subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

运行应用程序,控制台输出:

Error Domain=error Code=-100 "(null)" UserInfo={value=发生错误。}

合起来

除了使用属性executionSignals拿到内部信号并进行订阅,我们注意执行方法execute:有返回值并且是一个RACSignal类型的对象。实际上,这个返回对象就是那个内部信号,我们可以使用它直接进行订阅:

// command的创建需要一个信号,这个信号用来反向传递数据
RACCommand* command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
    NSLog(@"%@",input);
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        // 模拟网络请求
        [RACScheduler.currentScheduler afterDelay:1.0 schedule:^{
            [RACScheduler.mainThreadScheduler schedule:^{
                [subscriber sendNext:@"RACSignal信号发出消息。"];
                [subscriber sendError:[[NSError alloc] initWithDomain:@"error" code:-100 userInfo:@{@"value":@"发生错误。"}]];
            }];
        }];
        return nil;
    }];
}];

// command传递信息
[[command execute:@"RACCommand传递过来的信息"] subscribeNext:^(id x) {
    NSLog(@"%@",x);
} error:^(NSError *error) {
    NSLog(@"%@",error);
}];

运行应用程序,控制台输出:

RACCommand传递过来的信息
RACSignal信号发出消息。
Error Domain=error Code=-100 "(null)" UserInfo={value=发生错误。}

另一个初始化方法

RACCommand初始化一共有两种,另外一种:

- (id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock;

该初始化方法除了需要你传入一个反馈信号之外,还需要传入一个传递布尔值事件的RACSignal,这个信号作用是过滤,当传递的布尔值为真时,command能够执行,反之则不行。

UIButton的扩展属性rac_command如果绑定的是使用上述方式创建的command,那么button的enable属性会随着command的可执行性而改变,意思是当传递布尔值为真时,按钮才能使用。另外,当你按下按钮,command开始执行时,按钮的enable被自动设置成来NO,除非command执行完成,即发生completed事件。

当UIbutton的rac_command已经绑定了上述方法生成的command,那么你就不能动态改变按钮的enableRAC(self.button, enable) = someSignal;

UIButton 的 rac_command

UIButton+RACCommandSupport.h中,给UIButton进行了扩展rac_command,给按钮绑定上RACCommand,可以在按钮点击时自动执行execute:

self.signInButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            [subscriber sendNext:@"按钮执行结果"];
            [subscriber sendCompleted];
            return nil;
        }];
    }];
   
[self.signInButton.rac_command.executionSignals.switchToLatest subscribeNext:^(id x) {
        NSLog(@"%@",x);
}];

运行应用程序,在点击按钮时,控制台会输出:

按钮执行结果

如果你使用的是带有enabledSignal参数创建的RACCommand,它还会自动使你的按钮enable参数变化,直到你在信号中的事件完成并发生completederror事件。

// 创建 enabledSignal, 按钮可用 signal
RACSignal* signal = [self.usernameTextField.rac_textSignal map:^id(id value) {
    return @([value length]>3);
}];
self.signInButton.rac_command = [[RACCommand alloc] initWithEnabled:signal signalBlock:^RACSignal *(id input) {
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"按钮执行结果"];
            [subscriber sendCompleted];
//                [subscriber sendError:[NSError errorWithDomain:@"https://" code:-1 userInfo:@{}]];
        });
        return nil;
    }];
}];
[self.signInButton.rac_command.executionSignals.switchToLatest subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

只有在文本框中输入的字符大于3个时,按钮才能够使用。

RACCommand和RACSubject的差异

  • RACSubject只能单向发送事件,发送者(被监听者)将事件发送出去,让接收者们接收事件后进行处理。
  • RACCommand是双向的,常用作网络请求、操作数据库读写、按钮的操作事件等。当你想要向某个对象发送消息(操作),并需要该对象作出反馈时,你需要用到RACCommand,其中的内部信号将反馈变成了可能。

下面使用一张图来表明两者的差别和使用情况:

RACSubject与RACCommand

RACScheduler

// 默认优先级下的调度运行
[RACScheduler.scheduler schedule:^{
    // 获取当前调度的名称
    NSLog(@"%@",[[RACScheduler currentScheduler] valueForKey:@"name"]);
    // 回到主线程更新UI等
    [RACScheduler.mainThreadScheduler schedule:^{
        // do something ...
    }];
}];

RACScheduler是一个线性执行队列,ReactiveCocoa 中的信号可以在RACScheduler上执行任务、发送结果。

RACScheduler类的内部只有一个用于追踪标记和debug的属性name,你可以通过KVC的方式访问该属性。

我们可以将RACScheduler中的方法分为两类,一类是用于初始化RACScheduler实例的初始化方法:

+immediateScheduler //立刻执行
+mainThreadScheduler //主线程
+schedulerWithPriority:name: //自定义
+currentScheduler //当前的

其中+schedulerWithPriority:name:衍生出了几个具有默认参数的快捷方式,你可以在RACScheduler.h找到。

另外一类就是用于调度、执行任务的方法:

-schedule:
-after:schedule:
-afterDelay:schedule:
-after:repeatingEvery:withLeeway:schedule:
-scheduleRecursiveBlock:

其他一些示例:

// 低优先级下的调度运行
[[RACScheduler schedulerWithPriority:RACSchedulerPriorityLow name:@"custom scheduler"]
 schedule:^{
     NSLog(@"%@",[[RACScheduler currentScheduler] valueForKey:@"name"]);
 }];

// 延迟指定时长进行操作
[RACScheduler.scheduler afterDelay:5.0 schedule:^{
    NSLog(@"延迟5秒后输出");
}];

// 指定时间操作
[RACScheduler.scheduler after:[NSDate dateWithTimeIntervalSinceNow:2.0] schedule:^{
    NSLog(@"指定2秒后输出");
}];

// 定时器
[RACScheduler.currentScheduler after:[NSDate date] repeatingEvery:1.0 withLeeway:0 schedule:^{
    NSLog(@"重复输出");
}];

方法-after:repeatingEvery:withLeeway:schedule:中明确指出,在+ immediateScheduler上调用此方法被视为未定义的行为。

RACSignal中的定时器

在上一节中介绍了RACScheduler,其中演示了定时器的使用。ReactiveCocoa也同样给信号扩展了类似的方法,从信号的角度来执行定时调度的任务。

+interval:onScheduler:
+interval:onScheduler:withLeeway:

使用示例:

[[RACSignal interval:2.0 onScheduler:RACScheduler.currentScheduler withLeeway:10]
 subscribeNext:^(id x) {
     NSLog(@"%@",x);
 }];

运行应用程序,控制台输出:

Mon Sep 16 11:19:58 2019
Mon Sep 16 11:20:00 2019
Mon Sep 16 11:20:03 2019
Mon Sep 16 11:20:05 2019
Mon Sep 16 11:20:06 2019
Mon Sep 16 11:20:08 2019
Mon Sep 16 11:20:11 2019
Mon Sep 16 11:20:13 2019

从上面看出,RACSignal传递过来的是每次调用的日期数据,另外,我们发现,在Leeway延迟执行时间为10秒时,重复执行的时间间隔非常的不稳定,你可以注意你的控制台输出的前半部分的详情输出时间,你会发现误差超过了0.5秒以上。该方法注释中也指出了这个问题Note that some additional latency is to be expected, even when specifying aleewayof 0. 。即使指定leeway为0,也可能存在预期一些额外的延迟。

这可能是RACScheduler内部的消耗使时间误差加大,但是如果你使用场景不需要如此精确度,还是可以方便地作为定时器来使用。

onScheduler:参数不能为nil以及RACScheduler.immediateScheduler

取消定时器任务:

RACDisposable* disposable = [[RACSignal interval:2.0 onScheduler:RACScheduler.currentScheduler withLeeway:10]
 subscribeNext:^(id x) {
     NSLog(@"%@",x);
 }];

// 将来的某个时刻
[disposable dispose];

总结

ReactiveCocoa 对 KVO 的实现,让我们方便的监听对象的属性变化,因为这一点,ReactiveCocoa 成为 MVVM 结构的基础让我们所熟知。
RACSubject 和 RACCommand 是非 RAC 转为 RAC 的桥梁,前者可以手动控制发送事件,它的应用类似通知,可以被多次订阅,消息则发给多个订阅者。后者属于双向事件,消息并且需要反馈时使用它,它的应用场景如异步请求事件、操作数据库等。
RACScheduler 是 ReactiveCocoa 的关于线程调度对象,和事件流所被驱动的线程有关,是一个副产品。

相关阅读

相关文章

  • iOS ReactiveCocoa(RAC)番外篇

    在上一篇ReactiveCocoa(RAC)教程中,我们学习了ReactiveCocoa的一些基础内容。本篇中我们...

  • 2019-12-10

    iOS开发之RAC(一)初级篇 一、RAC是什么? 1、RAC全称:ReactiveCocoa, Github 一...

  • RAC 的使用

    RAC(ReactiveCocoa) 使用详解 RAC 是什么? ReactiveCocoa(RAC) githu...

  • ReactiveCocoa相关

    1、ReactiveCocoa简介。ReactiveCocoa简称RAC,是由Github开源的一个应用于iOS和...

  • 【other】Rac EventBus RxJava

    聊一聊 Rac Rac 是什么 Rac 全称 Reactivecocoa,是一个应用于iOS和OS X开发的框架,...

  • ReactiveCocoa基础

    ReactiveCocoa简介 ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS...

  • 15.ReactiveCocoa

    ReactiveCocoa简介 ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS...

  • 初识ReactiveCocoa(一) —— OC项目集成

    先简单介绍下ReactiveCocoa ReactiveCocoa(简称RAC)是Github上一套作用于iOS应...

  • ReactiveCocoa

    ReactiveCocoa简介 ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS...

  • ReactiveCocoa入门

    ReactiveCocoa介绍 ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS...

网友评论

      本文标题:iOS ReactiveCocoa(RAC)番外篇

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