ReactiveCocoa进阶

From: https://vinefiner.github.io/2017/01/06/ReactiveCocoa-%E8%BF%9B%E9%98%B6/

上篇文章中介绍了ReactiveCocoa的基础知识,接下来我们来深入介绍ReactiveCocoa及其在MVVM中的用法。

ReactiveCocoa进阶思维导图

操作须知

所有的信号(RACSignal)都可以进行操作处理,因为所有操作方法都定义在RACStream.h中,因此只要继承RACStream就有了操作处理方法。

操作思想

运用的是Hook(钩子)思想,Hook是一种用于改变API(应用程序编程接口:方法)执行结果的技术.

Hook用处:截获API调用的技术。

有关Hook的知识可以看我的这篇博客《Objective-C Runtime 的一些基本使用》中的 更换代码的实现方法 一节,

Hook原理:在每次调用一个API返回结果之前,先执行你自己的方法,改变结果的输出。

操作方法

bind(绑定)- ReactiveCocoa核心方法

ReactiveCocoa 操作的核心方法是 bind(绑定),而且也是RAC中核心开发方式。之前的开发方式是赋值,而用RAC开发,应该把重心放在绑定,也就是可以在创建一个对象的时候,就绑定好以后想要做的事情,而不是等赋值之后在去做事情。

列如,把数据展示到控件上,之前都是重写控件的 setModel 方法,用RAC就可以在一开始创建控件的时候,就绑定好数据。

  • 作用

RAC底层都是调用bind, 在开发中很少直接使用 bind 方法,bind属于RAC中的底层方法,我们只需要调用封装好的方法,bind用作了解即可.

  • bind方法使用步骤
    1. 传入一个返回值 RACStreamBindBlock 的 block。
    2. 描述一个 RACStreamBindBlock 类型的 bindBlock作为block的返回值。
    3. 描述一个返回结果的信号,作为 bindBlock 的返回值。

注意:在bindBlock中做信号结果的处理。

  • bind方法参数

RACStreamBindBlock: typedef RACStream * (^RACStreamBindBlock)(id value, BOOL *stop);

参数一(value):表示接收到信号的原始值,还没做处理

参数二(*stop):用来控制绑定Block,如果*stop = yes,那么就会结束绑定。

返回值:信号,做好处理,在通过这个信号返回出去,一般使用 RACReturnSignal,需要手动导入头文件RACReturnSignal.h

  • 使用

假设想监听文本框的内容,并且在每次输出结果的时候,都在文本框的内容拼接一段文字“输出:”

* 使用封装好的方法:在返回结果后,拼接。 

  [_textField.rac_textSignal subscribeNext:^(id x) {

      // 在返回结果后,拼接 输出:
      NSLog(@"输出:%@",x);

  }];


* 方式二:,使用RAC中 `bind` 方法做处理,在返回结果前,拼接。 

这里需要手动导入#import <ReactiveCocoa/RACReturnSignal.h>,才能使用RACReturnSignal

[[_textField.rac_textSignal bind:^RACStreamBindBlock{
   // 什么时候调用:
   // block作用:表示绑定了一个信号.

   return ^RACStream *(id value, BOOL *stop){

       // 什么时候调用block:当信号有新的值发出,就会来到这个block。

       // block作用:做返回值的处理

       // 做好处理,在返回结果前,拼接 输出:
       return [RACReturnSignal return:[NSString stringWithFormat:@"输出:%@",value]];
   };

}] subscribeNext:^(id x) {

   NSLog(@"%@",x);

}];
  • 底层实现
    1. 源信号调用bind,会重新创建一个绑定信号。
    2. 当绑定信号被订阅,就会调用绑定信号中的 didSubscribe ,生成一个 bindingBlock
    3. 当源信号有内容发出,就会把内容传递到 bindingBlock 处理,调用bindingBlock(value,stop)
    4. 调用bindingBlock(value,stop),会返回一个内容处理完成的信号RACReturnSignal
    5. 订阅RACReturnSignal,就会拿到绑定信号的订阅者,把处理完成的信号内容发送出来。

注意:不同订阅者,保存不同的nextBlock,看源码的时候,一定要看清楚订阅者是哪个。

映射

映射主要用这两个方法实现:flattenMap,Map,用于把源信号内容映射成新的内容。

flattenMap
  • 作用

把源信号的内容映射成一个新的信号,信号可以是任意类型

  • 使用步骤

    1. 传入一个block,block类型是返回值RACStream,参数value
    2. 参数value就是源信号的内容,拿到源信号的内容做处理
    3. 包装成RACReturnSignal信号,返回出去。
  • 使用

监听文本框的内容改变,把结构重新映射成一个新值.

[[_textField.rac_textSignal flattenMap:^RACStream *(id value) {

    // block调用时机:信号源发出的时候

    // block作用:改变信号的内容

    // 返回RACReturnSignal
    return [RACReturnSignal return:[NSString stringWithFormat:@"信号内容:%@", value]];

}] subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];
  • 底层实现

    1. flattenMap内部调用 bind 方法实现的,flattenMap中block的返回值,会作为bind中bindBlock的返回值。
    2. 当订阅绑定信号,就会生成 bindBlock
    3. 当源信号发送内容,就会调用 bindBlock(value, *stop)
    4. 调用bindBlock,内部就会调用 flattenMap 的 bloc k,flattenMap 的block作用:就是把处理好的数据包装成信号。
    5. 返回的信号最终会作为 bindBlock 中的返回信号,当做 bindBlock 的返回信号。
    6. 订阅 bindBlock 的返回信号,就会拿到绑定信号的订阅者,把处理完成的信号内容发送出来。
Map
  • 作用

把源信号的值映射成一个新的值

  • 使用步骤
    1. 传入一个block,类型是返回对象,参数是 value
    2. value就是源信号的内容,直接拿到源信号的内容做处理
    3. 把处理好的内容,直接返回就好了,不用包装成信号,返回的值,就是映射的值。
  • 使用

监听文本框的内容改变,把结构重新映射成一个新值.

[[_textField.rac_textSignal map:^id(id value) {

   // 拼接完后,返回对象
    return [NSString stringWithFormat:@"信号内容: %@", value];

}] subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];
  • 底层实现:
    1. Map底层其实是调用 flatternMap,Map 中block中的返回的值会作为 flatternMap 中block中的值
    2. 当订阅绑定信号,就会生成 bindBlock
    3. 当源信号发送内容,就会调用 bindBlock(value, *stop)
    4. 调用 bindBlock ,内部就会调用 flattenMap的block
    5. flattenMap的block 内部会调用 Map 中的block,把 Map 中的block返回的内容包装成返回的信号
    6. 返回的信号最终会作为 bindBlock 中的返回信号,当做 bindBlock 的返回信号
    7. 订阅 bindBlock 的返回信号,就会拿到绑定信号的订阅者,把处理完成的信号内容发送出来。
FlatternMap 和 Map 的区别
  • FlatternMap 中的Block 返回信号
    1. Map 中的Block 返回对象
    2. 开发中,如果信号发出的值 不是信号 ,映射一般使用 Map
    3. 如果信号发出的值 是信号,映射一般使用 FlatternMap
  • signalOfsignalsFlatternMap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 创建信号中的信号
    RACSubject *signalOfsignals = [RACSubject subject];
    RACSubject *signal = [RACSubject subject];
    [[signalOfsignals flattenMap:^RACStream *(id value) {
    // 当signalOfsignals的signals发出信号才会调用
    return value;
    }] subscribeNext:^(id x) {
    // 只有signalOfsignals的signal发出信号才会调用,因为内部订阅了bindBlock中返回的信号,也就是flattenMap返回的信号。
    // 也就是flattenMap返回的信号发出内容,才会调用。
    NSLog(@"signalOfsignals:%@",x);
    }];
    // 信号的信号发送信号
    [signalOfsignals sendNext:signal];
    // 信号发送内容
    [signal sendNext:@"hi"];

组合

组合就是将多个信号按照某种规则进行拼接,合成新的信号。

  • 作用

顺序拼接信号,当多个信号发出的时候,有顺序的接收信号。

  • 底层实现
    1. 当拼接信号被订阅,就会调用拼接信号的didSubscribe
    2. didSubscribe中,会先订阅第一个源信号(signalA)
    3. 会执行第一个源信号(signalA)的didSubscribe
    4. 第一个源信号(signalA)didSubscribe中发送值,就会调用第一个源信号(signalA)订阅者的nextBlock,通过拼接信号的订阅者把值发送出来.
    5. 第一个源信号(signalA)didSubscribe中发送完成,就会调用第一个源信号(signalA)订阅者的completedBlock,订阅第二个源信号(signalB)这时候才激活(signalB)。
    6. 订阅第二个源信号(signalB),执行第二个源信号(signalB)的didSubscribe
    7. 第二个源信号(signalA)didSubscribe中发送值,就会通过拼接信号的订阅者把值发送出来.
  • 使用步骤

    1. 使用concat:拼接信号
    2. 订阅拼接信号,内部会自动按拼接顺序订阅信号
  • 使用

拼接信号 signalAsignalBsignalC

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    [subscriber sendNext:@"Hello"];

    [subscriber sendCompleted];

    return nil;
}];

RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    [subscriber sendNext:@"World"];

    [subscriber sendCompleted];

    return nil;
}];

RACSignal *signalC = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    [subscriber sendNext:@"!"];

    [subscriber sendCompleted];

    return nil;
}];

// 拼接 A B, 把signalA拼接到signalB后,signalA发送完成,signalB才会被激活。
RACSignal *concatSignalAB = [signalA concat:signalB];

// A B + C
RACSignal *concatSignalABC = [concatSignalAB concat:signalC];


// 订阅拼接的信号, 内部会按顺序订阅 A->B->C
// 注意:第一个信号必须发送完成,第二个信号才会被激活...
[concatSignalABC subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];
then
  • 作用

用于连接两个信号,当第一个信号完成,才会连接then返回的信号。

  • 底层实现

    1. 先过滤掉之前的信号发出的值
    2. 使用concat连接then返回的信号
  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@1];
    [subscriber sendCompleted];
    return nil;
    }] then:^RACSignal *{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@2];
    return nil;
    }];
    }] subscribeNext:^(id x) {
    // 只能接收到第二个信号的值,也就是then返回信号的值
    NSLog(@"%@", x);
    }];
    ///
    输出:2
  • 注意

注意使用then,之前信号的值会被忽略掉.

merge
  • 作用

合并信号,任何一个信号发送数据,都能监听到.

  • 底层实现

    1. 合并信号被订阅的时候,就会遍历所有信号,并且发出这些信号。
    2. 每发出一个信号,这个信号就会被订阅
    3. 也就是合并信号一被订阅,就会订阅里面所有的信号。
    4. 只要有一个信号被发出就会被监听。
  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"A"];
    return nil;
    }];
    RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"B"];
    return nil;
    }];
    // 合并信号, 任何一个信号发送数据,都能监听到
    RACSignal *mergeSianl = [signalA merge:signalB];
    [mergeSianl subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    // 输出
    2017-01-03 13:29:08.013 ReactiveCocoa进阶[3627:718315] A
    2017-01-03 13:29:08.014 ReactiveCocoa进阶[3627:718315] B
zip
  • 作用

把两个信号压缩成一个信号,只有当两个信号 同时 发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发压缩流的next事件。

  • 底层实现

    1. 定义压缩信号,内部就会自动订阅signalA,signalB
    2. 每当signalA或者signalB发出信号,就会判断signalA,signalB有没有发出个信号,有就会把每个信号 第一次 发出的值包装成元组发出
  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"A1"];
    [subscriber sendNext:@"A2"];
    return nil;
    }];
    RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"B1"];
    [subscriber sendNext:@"B2"];
    [subscriber sendNext:@"B3"];
    return nil;
    }];
    RACSignal *zipSignal = [signalA zipWith:signalB];
    [zipSignal subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    // 输出
    2017-01-03 13:48:09.234 ReactiveCocoa进阶[3997:789720] zipWith: <RACTuple: 0x600000004df0> (
    A1,
    B1
    )
    2017-01-03 13:48:09.234 ReactiveCocoa进阶[3997:789720] zipWith: <RACTuple: 0x608000003410> (
    A2,
    B2
    )
    ```
    ###### combineLatest
    * **作用**
    将多个信号合并起来,并且拿到各个信号最后一个值,必须每个合并的signal至少都有过一次sendNext,才会触发合并的信号。
    * **底层实现**
    1. 当组合信号被订阅,内部会自动订阅signalA,signalB,必须两个信号都发出内容,才会被触发。 2. 并且把两个信号的 最后一次 发送的值组合成元组发出。
    * **使用**

    RACSignal signalA = [RACSignal createSignal:^RACDisposable (id subscriber) {

    [subscriber sendNext:@"A1"];
    [subscriber sendNext:@"A2"];
    
    return nil;
    

    }];

    RACSignal signalB = [RACSignal createSignal:^RACDisposable (id subscriber) {

    [subscriber sendNext:@"B1"];
    [subscriber sendNext:@"B2"];
    [subscriber sendNext:@"B3"];
    
    return nil;
    

    }];

    RACSignal *combineSianal = [signalA combineLatestWith:signalB];

    [combineSianal subscribeNext:^(id x) {

    NSLog(@"combineLatest:%@", x);
    

    }];

    // 输出
    2017-01-03 13:48:09.235 ReactiveCocoa进阶[3997:789720] combineLatest: (
    A2,
    B1
    )
    2017-01-03 13:48:09.235 ReactiveCocoa进阶[3997:789720] combineLatest: (
    A2,
    B2
    )
    2017-01-03 13:48:09.236 ReactiveCocoa进阶[3997:789720] combineLatest: (
    A2,
    B3
    )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    * **注意**
    **combineLatest**与**zip**用法相似,必须每个合并的signal至少都有过一次sendNext,才会触发合并的信号。
    区别看下图:
    ![][4]
    [4]: http://images.chenzhao.date/blogimages/ReactiveCocoa%20%E8%BF%9B%E9%98%B6/pic_002.jpg
    ###### reduce
    * **作用**
    把信号发出元组的值聚合成一个值
    * **底层实现**
    1. 订阅聚合信号, 2. 每次有内容发出,就会执行reduceblcok,把信号内容转换成reduceblcok返回的值。
    * **使用**
    常见的用法,(先组合在聚合)`combineLatest:(id<NSFastEnumeration>)signals reduce:(id (^)())reduceBlock`
    reduce中的block简介:
    reduceblcok中的参数,有多少信号组合,reduceblcok就有多少参数,每个参数就是之前信号发出的内容 reduceblcok的返回值:聚合信号之后的内容。
    RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"A1"];
    [subscriber sendNext:@"A2"];
    return nil;
    }];
    RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"B1"];
    [subscriber sendNext:@"B2"];
    [subscriber sendNext:@"B3"];
    return nil;
    }];
    RACSignal *reduceSignal = [RACSignal combineLatest:@[signalA, signalB] reduce:^id(NSString *str1, NSString *str2){
    return [NSString stringWithFormat:@"%@ %@", str1, str2];
    }];
    [reduceSignal subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    // 输出
    2017-01-03 15:42:41.803 ReactiveCocoa进阶[4248:1264674] A2 B1
    2017-01-03 15:42:41.803 ReactiveCocoa进阶[4248:1264674] A2 B2
    2017-01-03 15:42:41.803 ReactiveCocoa进阶[4248:1264674] A2 B3
    #### 过滤
    过滤就是过滤信号中的 特定值 ,或者过滤指定 发送次数 的信号。
    ###### filter
    * **作用**
    过滤信号,使用它可以获取满足条件的信号.
    block的返回值是Bool值,返回`NO`则过滤该信号
    * **使用**

    // 过滤:
    // 每次信号发出,会先执行过滤条件判断.
    [[_textField.rac_textSignal filter:^BOOL(NSString *value) {

    NSLog(@"原信号: %@", value);
    
    // 过滤 长度 <= 3 的信号
    return value.length > 3;
    

    }] subscribeNext:^(id x) {

    NSLog(@"长度大于3的信号:%@", x);
    

    }];

    // 在_textField中输出12345
    // 输出
    2017-01-03 16:36:54.938 ReactiveCocoa进阶[4714:1552910] 原信号: 1
    2017-01-03 16:36:55.383 ReactiveCocoa进阶[4714:1552910] 原信号: 12
    2017-01-03 16:36:55.706 ReactiveCocoa进阶[4714:1552910] 原信号: 123
    2017-01-03 16:36:56.842 ReactiveCocoa进阶[4714:1552910] 原信号: 1234
    2017-01-03 16:36:56.842 ReactiveCocoa进阶[4714:1552910] 长度大于3的信号:1234
    2017-01-03 16:36:58.350 ReactiveCocoa进阶[4714:1552910] 原信号: 12345
    2017-01-03 16:36:58.351 ReactiveCocoa进阶[4714:1552910] 长度大于3的信号:12345

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    ###### ignore
    * **作用**
    忽略某些信号.
    * **使用**
    * **作用**
    忽略某些值的信号.
    底层调用了 `filter` 与 过滤值进行比较,若相等返回则 `NO`
    * **使用**
    ``` // 内部调用filter过滤,忽略掉字符为 @“1”的值 [[_textField.rac_textSignal ignore:@”1”] subscribeNext:^(id x) {
    NSLog(@”%@”,x);
    }];
distinctUntilChanged
  • 作用

当上一次的值和当前的值有明显的变化就会发出信号,否则会被忽略掉。

  • 使用

    1
    2
    3
    4
    5
    [[_textField.rac_textSignal distinctUntilChanged] subscribeNext:^(id x) {
    NSLog(@"%@",x);
    }];
skip
  • 作用

跳过 第N次 的发送的信号.

  • 使用
1
2
3
4
5
// 表示输入第一次,不会被监听到,跳过第一次发出的信号
[[_textField.rac_textSignal skip:1] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
take
  • 作用

前N次 的发送的信号.

  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    RACSubject *subject = [RACSubject subject] ;
    // 取 前两次 发送的信号
    [[subject take:2] subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    [subject sendNext:@1];
    [subject sendNext:@2];
    [subject sendNext:@3];
    // 输出
    2017-01-03 17:35:54.566 ReactiveCocoa进阶[4969:1677908] 1
    2017-01-03 17:35:54.567 ReactiveCocoa进阶[4969:1677908] 2
takeLast
  • 作用

最后N次 的发送的信号

前提条件,订阅者必须调用完成 sendCompleted,因为只有完成,就知道总共有多少信号.

  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    RACSubject *subject = [RACSubject subject] ;
    // 取 后两次 发送的信号
    [[subject takeLast:2] subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    [subject sendNext:@1];
    [subject sendNext:@2];
    [subject sendNext:@3];
    // 必须 跳用完成
    [subject sendCompleted];
    ```
    ###### takeUntil
    * **作用**
    获取信号直到某个信号执行完成
    * **使用**
    ```
    // 监听文本框的改变直到当前对象被销毁
    [_textField.rac_textSignal takeUntil:self.rac_willDeallocSignal];
    ```
    ###### switchToLatest
    * **作用**
    用于signalOfSignals(信号的信号),有时候信号也会发出信号,会在signalOfSignals中,获取signalOfSignals发送的最新信号。
    * **注意**
    switchToLatest:只能用于信号中的信号
    * **使用**

    RACSubject signalOfSignals = [RACSubject subject];
    RACSubject
    signal = [RACSubject subject];

    // 获取信号中信号最近发出信号,订阅最近发出的信号。
    [signalOfSignals.switchToLatest subscribeNext:^(id x) {

    NSLog(@"%@", x);
    

    }];

    [signalOfSignals sendNext:signal];
    [signal sendNext:@1];

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    #### 秩序
    秩序包括 `doNext` 和 `doCompleted` 这两个方法,主要是在 执行`sendNext` 或者 `sendCompleted`之前,先执行这些方法中Block。
    ###### doNext
    执行`sendNext`之前,会先执行这个`doNext`的 Block
    ###### doCompleted
    执行`sendCompleted`之前,会先执行这`doCompleted`的`Block`
    [[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"hi"];
    [subscriber sendCompleted];
    return nil;
    }] doNext:^(id x) {
    // 执行 [subscriber sendNext:@"hi"] 之前会调用这个 Block
    NSLog(@"doNext");
    }] doCompleted:^{
    // 执行 [subscriber sendCompleted] 之前会调用这 Block
    NSLog(@"doCompleted");
    }] subscribeNext:^(id x) {
    NSLog(@"%@", x);
    }];
    #### 线程
    **ReactiveCocoa** 中的线程操作 包括 `deliverOn` 和 `subscribeOn`这两种,将 _传递的内容_ 或 创建信号时 _block中的代码_ 切换到指定的线程中执行。
    ###### deliverOn
    * **作用**
    内容传递切换到制定线程中,副作用在原来线程中,把在创建信号时block中的代码称之为副作用。
    * **使用**
  // 在子线程中执行
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

      [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

          NSLog(@"%@", [NSThread currentThread]);

          [subscriber sendNext:@123];

          [subscriber sendCompleted];

          return nil;
      }]
        deliverOn:[RACScheduler mainThreadScheduler]]

       subscribeNext:^(id x) {

           NSLog(@"%@", x);

           NSLog(@"%@", [NSThread currentThread]);
       }];
  });

  // 输出
2017-01-04 10:35:55.415 ReactiveCocoa进阶[1183:224535] <NSThread: 0x608000270f00>{number = 3, name = (null)}
2017-01-04 10:35:55.415 ReactiveCocoa进阶[1183:224482] 123
2017-01-04 10:35:55.415 ReactiveCocoa进阶[1183:224482] <NSThread: 0x600000079bc0>{number = 1, name = main}
1
2
3
4
5
6
7
8
9
10
可以看到`副作用`_子线程_ 中执行,而 `传递的内容`_主线程_ 中接收
###### subscribeOn
* **作用**
**subscribeOn**则是将 `内容传递``副作用` 都会切换到指定线程中
* **使用**
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

      [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

          NSLog(@"%@", [NSThread currentThread]);

          [subscriber sendNext:@123];

          [subscriber sendCompleted];

          return nil;
      }]
        subscribeOn:[RACScheduler mainThreadScheduler]] //传递的内容到主线程中
       subscribeNext:^(id x) {

           NSLog(@"%@", x);

           NSLog(@"%@", [NSThread currentThread]);
       }];
  });   
  //
2017-01-04 10:44:47.558 ReactiveCocoa进阶[1243:275126] <NSThread: 0x608000077640>{number = 1, name = main}
2017-01-04 10:44:47.558 ReactiveCocoa进阶[1243:275126] 123
2017-01-04 10:44:47.558 ReactiveCocoa进阶[1243:275126] <NSThread: 0x608000077640>{number = 1, name = main}

```

内容传递副作用 都切换到了 主线程 执行

时间

时间操作就会设置信号超时,定时和延时。

interval 定时
  • 作用

定时:每隔一段时间发出信号

// 每隔1秒发送信号,指定当前线程执行
[[RACSignal interval:1 onScheduler:[RACScheduler currentScheduler]] subscribeNext:^(id x) {

    NSLog(@"定时:%@", x);
}];

// 输出
2017-01-04 13:48:55.196 ReactiveCocoa进阶[1980:492724] 定时:2017-01-04 05:48:55 +0000
2017-01-04 13:48:56.195 ReactiveCocoa进阶[1980:492724] 定时:2017-01-04 05:48:56 +0000
2017-01-04 13:48:57.196 ReactiveCocoa进阶[1980:492724] 定时:2017-01-04 05:48:57 +0000
  • 作用

超时,可以让一个信号在一定的时间后,自动报错。

RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    // 不发送信号,模拟超时状态
    // [subscriber sendNext:@"hello"];
    //[subscriber sendCompleted];

    return nil;
}] timeout:1 onScheduler:[RACScheduler currentScheduler]];// 设置1秒超时

[signal subscribeNext:^(id x) {

    NSLog(@"%@", x);
} error:^(NSError *error) {

    NSLog(@"%@", error);
}];

// 执行代码 1秒后 输出:
2017-01-04 13:48:55.195 ReactiveCocoa进阶[1980:492724] Error Domain=RACSignalErrorDomain Code=1 "(null)"
delay 延时
  • 作用

延时,延迟一段时间后发送信号

RACSignal *signal2 = [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    [subscriber sendNext:@"延迟输出"];

    return nil;
}] delay:2] subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];

// 执行代码 2秒后 输出
2017-01-04 13:55:23.751 ReactiveCocoa进阶[2030:525038] 延迟输出

重复

retry
  • 作用

重试:只要 发送错误 sendError:,就会 重新执行 创建信号的Block 直到成功

  __block int i = 0;

  [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

      if (i == 5) {

          [subscriber sendNext:@"Hello"];

      } else {

          // 发送错误
          NSLog(@"收到错误:%d", i);
          [subscriber sendError:nil];
      }

      i++;

      return nil;

  }] retry] subscribeNext:^(id x) {

      NSLog(@"%@", x);

  } error:^(NSError *error) {

      NSLog(@"%@", error);

  }];

  // 输出
2017-01-04 14:36:51.594 ReactiveCocoa进阶[2443:667226] 收到错误信息:0
2017-01-04 14:36:51.595 ReactiveCocoa进阶[2443:667226] 收到错误信息:1
2017-01-04 14:36:51.595 ReactiveCocoa进阶[2443:667226] 收到错误信息:2
2017-01-04 14:36:51.596 ReactiveCocoa进阶[2443:667226] 收到错误信息:3
2017-01-04 14:36:51.596 ReactiveCocoa进阶[2443:667226] 收到错误信息:4
2017-01-04 14:36:51.596 ReactiveCocoa进阶[2443:667226] Hello
replay
  • 作用

重放:当一个信号被多次订阅,反复播放内容

  RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

      [subscriber sendNext:@1];
      [subscriber sendNext:@2];

      return nil;
  }] replay];

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

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

  // 输出
2017-01-04 14:51:01.934 ReactiveCocoa进阶[2544:706740] 1
2017-01-04 14:51:01.934 ReactiveCocoa进阶[2544:706740] 2
2017-01-04 14:51:01.934 ReactiveCocoa进阶[2544:706740] 1
2017-01-04 14:51:01.935 ReactiveCocoa进阶[2544:706740] 2
throttle
  • 作用

节流:当某个信号发送比较频繁时,可以使用节流,在某一段时间不发送信号内容,过了一段时间获取信号的最新内容发出。

RACSubject *subject = [RACSubject subject];

// 节流1秒,1秒后接收最后一个发送的信号
[[subject throttle:1] subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];

[subject sendNext:@1];
[subject sendNext:@2];
[subject sendNext:@3];

// 输出
2017-01-04 15:02:37.543 ReactiveCocoa进阶[2731:758193] 3

程序为什么要有架构?便于程序开发与维护.

常见的架构

  • MVC

M:模型 V:视图 C:控制器

  • MVVM

M:模型 V:视图+控制器 VM:视图模型

  • MVCS

M:模型 V:视图 C:控制器 C:服务类

V:视图 I:交互器 P:展示器 E:实体 R:路由

MVVM介绍

  • 模型(M):保存视图数据。

  • 视图+控制器(V):展示内容 + 如何展示

  • 视图模型(VM):处理展示的业务逻辑,包括按钮的点击,数据的请求和解析等等。

需求

  1. 监听两个文本框的内容
  2. 有内容登录按键才允许按钮点击
  3. 返回登录结果

分析

  1. 界面的所有业务逻辑都交给控制器做处理
  2. 在MVVM架构中把控制器的业务全部搬去VM模型,也就是每个控制器对应一个VM模型.

步骤

  1. 创建LoginViewModel类,处理登录界面业务逻辑.
  2. 这个类里面应该保存着账号的信息,创建一个账号Account模型
  3. LoginViewModel应该保存着账号信息Account模型。
  4. 需要时刻监听Account模型中的账号和密码的改变,怎么监听?
  5. 在非RAC开发中,都是习惯赋值,在RAC开发中,需要改变开发思维,由赋值转变为绑定,可以在一开始初始化的时候,就给Account模型中的属性绑定,并不需要重写set方法。
  6. 每次Account模型的值改变,就需要判断按钮能否点击,在VM模型中做处理,给外界提供一个能否点击按钮的信号.
  7. 这个登录信号需要判断Account中账号和密码是否有值,用KVO监听这两个值的改变,把他们聚合成登录信号.
  8. 监听按钮的点击,由VM处理,应该给VM声明一个RACCommand,专门处理登录业务逻辑.
  9. 执行命令,把数据包装成信号传递出去
  10. 监听命令中信号的数据传递
  11. 监听命令的执行时刻

运行效果

登录界面

代码

MyViewController.m

#import "MyViewController.h"
#import "LoginViewModel.h"

@interface MyViewController ()

@property (nonatomic, strong) LoginViewModel *loginViewModel;

@property (weak, nonatomic) IBOutlet UITextField *accountField;

@property (weak, nonatomic) IBOutlet UITextField *pwdField;

@property (weak, nonatomic) IBOutlet UIButton *loginBtn;

@end

@implementation MyViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    [self bindModel];

}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}



// 视图模型绑定
- (void)bindModel {

    // 给模型的属性绑定信号
    //
    RAC(self.loginViewModel.account, account) = _accountField.rac_textSignal;
    RAC(self.loginViewModel.account, pwd) = _pwdField.rac_textSignal;

    RAC(self.loginBtn, enabled) = self.loginViewModel.enableLoginSignal;

    // 监听登录点击
    [[_loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {

        [self.loginViewModel.LoginCommand execute:nil];
    }];

}
- (IBAction)btnTap:(id)sender {


}

#pragma mark - lazyLoad

- (LoginViewModel *)loginViewModel {

    if (nil == _loginViewModel) {
        _loginViewModel = [[LoginViewModel alloc] init];
    }

    return _loginViewModel;
}

LoginViewModel.h

#import <UIKit/UIKit.h>

@interface Account : NSObject

@property (nonatomic, strong) NSString *account;
@property (nonatomic, strong) NSString *pwd;

@end


@interface LoginViewModel : UIViewController

@property (nonatomic, strong) Account *account;

// 是否允许登录的信号
@property (nonatomic, strong, readonly) RACSignal *enableLoginSignal;

@property (nonatomic, strong, readonly) RACCommand *LoginCommand;

@end

LoginViewModel.m

#import "LoginViewModel.h"

@implementation Account

@end


@interface LoginViewModel ()

@end

@implementation LoginViewModel

- (instancetype)init {

    if (self = [super init]) {
        [self initialBind];
    }
    return self;
}

- (void)initialBind {

    // 监听账号属性改变, 把他们合成一个信号
    _enableLoginSignal = [RACSubject combineLatest:@[RACObserve(self.account, account), RACObserve(self.account, pwd)] reduce:^id(NSString *accout, NSString *pwd){

        return @(accout.length && pwd.length);
    }];

    // 处理业务逻辑
    _LoginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {

        NSLog(@"点击了登录");
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

            // 模仿网络延迟

            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

                // 返回登录成功 发送成功信号
                [subscriber sendNext:@"登录成功"];
            });

            return nil;
        }];
    }];


    // 监听登录产生的数据
    [_LoginCommand.executionSignals.switchToLatest subscribeNext:^(id x) {

        if ([x isEqualToString:@"登录成功"]) {
            NSLog(@"登录成功");
        }

    }];

    [[_LoginCommand.executing skip:1] subscribeNext:^(id x) {

        if ([x isEqualToNumber:@(YES)]) {

            NSLog(@"正在登陆...");
        } else {

        // 登录成功
        NSLog(@"登陆成功");

        }

    }];
}

#pragma mark - lazyLoad

- (Account *)account
{
    if (_account == nil) {
        _account = [[Account alloc] init];
    }
    return _account;
}

- (void)viewDidLoad {
    [super viewDidLoad];

}

@end

需求

  1. 请求一段网络数据,将请求到的数据在tableView上展示
  2. 该数据为豆瓣图书的搜索返回结果,URL:url:https://api.douban.com/v2/book/search?q=悟空传

分析

  1. 界面的所有业务逻辑都交给控制器做处理
  2. 网络请求交给MV模型处理

步骤

  1. 控制器提供一个视图模型(requesViewModel),处理界面的业务逻辑
  2. VM提供一个命令,处理请求业务逻辑
  3. 在创建命令的block中,会把请求包装成一个信号,等请求成功的时候,就会把数据传递出去。
  4. 请求数据成功,应该把字典转换成模型,保存到视图模型中,控制器想用就直接从视图模型中获取。

其他

网络请求与图片缓存用到了[AFNetworking][7] 和 SDWebImage,自行在Pods中导入。

[7]: https://github.com/AFNetworking/AFNetworking

platform :ios, ‘8.0’

target 'ReactiveCocoa进阶' do

use_frameworks!
pod 'ReactiveCocoa', '~> 2.5'
pod 'AFNetworking'
pod 'SDWebImage'
end

运行效果

代码

SearchViewController.m

#import "SearchViewController.h"
#import "RequestViewModel.h"

@interface SearchViewController ()<UITableViewDataSource>

@property (nonatomic, strong) UITableView *tableView;

@property (nonatomic, strong) RequestViewModel *requesViewModel;

@end

@implementation SearchViewController

- (RequestViewModel *)requesViewModel
{
    if (_requesViewModel == nil) {
        _requesViewModel = [[RequestViewModel alloc] init];
    }
    return _requesViewModel;
}

- (void)viewDidLoad {
    [super viewDidLoad];


    self.tableView = [[UITableView alloc] initWithFrame:self.view.frame];

    self.tableView.dataSource = self;

    [self.view addSubview:self.tableView];

    //
    RACSignal *requesSiganl = [self.requesViewModel.reuqesCommand execute:nil];

    [requesSiganl subscribeNext:^(NSArray *x) {

        self.requesViewModel.models = x;

        [self.tableView reloadData];
    }];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.requesViewModel.models.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *ID = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    if (cell == nil) {

        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
    }

    Book *book = self.requesViewModel.models[indexPath.row];
    cell.detailTextLabel.text = book.subtitle;
    cell.textLabel.text = book.title;

    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:book.image] placeholderImage:[UIImage imageNamed:@"cellImage"]];


    return cell;
}
@end

RequestViewModel.h

#import <Foundation/Foundation.h>

@interface Book : NSObject

@property (nonatomic, copy) NSString *subtitle;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *image;

@end

@interface RequestViewModel : NSObject

// 请求命令
@property (nonatomic, strong, readonly) RACCommand *reuqesCommand;

//模型数组
@property (nonatomic, strong) NSArray *models;


@end

RequestViewModel.m

#import "RequestViewModel.h"

@implementation Book

- (instancetype)initWithValue:(NSDictionary *)value {

    if (self = [super init]) {

        self.title = value[@"title"];
        self.subtitle = value[@"subtitle"];
        self.image = value[@"image"];
    }
    return self;
}

+ (Book *)bookWithDict:(NSDictionary *)value {

    return [[self alloc] initWithValue:value];
}



@end

@implementation RequestViewModel

- (instancetype)init
{
    if (self = [super init]) {

        [self initialBind];
    }
    return self;
}


- (void)initialBind
{
    _reuqesCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {

      RACSignal *requestSiganl = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

          NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
          parameters[@"q"] = @"悟空传";

          //
          [[AFHTTPSessionManager manager] GET:@"https://api.douban.com/v2/book/search" parameters:parameters progress:^(NSProgress * _Nonnull downloadProgress) {

              NSLog(@"downloadProgress: %@", downloadProgress);
          } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

              // 数据请求成功就讲数据发送出去
              NSLog(@"responseObject:%@", responseObject);

              [subscriber sendNext:responseObject];

              [subscriber sendCompleted];

          } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

              NSLog(@"error: %@", error);
          }];


         return nil;
      }];

        // 在返回数据信号时,把数据中的字典映射成模型信号,传递出去
        return [requestSiganl map:^id(NSDictionary *value) {

            NSMutableArray *dictArr = value[@"books"];

            NSArray *modelArr = [[dictArr.rac_sequence map:^id(id value) {

                return [Book bookWithDict:value];

            }] array];

            return modelArr;

        }];

    }];
}


@end

最后附上GitHub:https://github.com/qiubaiying/ReactiveCocoa_Demo



ReactiveCocoa之基础篇

From: http://qiubaiying.top/2016/12/26/ReactiveCocoa-%E5%9F%BA%E7%A1%80/

本文修改自[最快让你上手ReactiveCocoa之基础篇][1]

[1]: http://www.jianshu.com/p/87ef6720a096

有关对 ReactiveCocoa 的看法可以看一下唐巧的这篇ReactiveCocoa 讨论会

ReactiveCocoa思维导图

ReactiveCocoa

ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS和OS开发的新框架,Cocoa是苹果整套框架的简称,因此很多苹果框架喜欢以Cocoa结尾。

在我们iOS开发过程中,当某些事件响应的时候,需要处理某些业务逻辑,这些事件都用不同的方式来处理。

比如按钮的点击使用action,ScrollView滚动使用delegate,属性值改变使用KVO等系统提供的方式。其实这些事件,都可以通过RAC处理

ReactiveCocoa为事件提供了很多处理方法,而且利用RAC处理事件很方便,可以把要处理的事情,和监听的事情的代码放在一起,这样非常方便我们管理,就不需要跳到对应的方法里。

非常符合我们开发中高聚合,低耦合的思想。

在开发中我们也不能太依赖于某个框架,否则这个框架不更新了,导致项目后期没办法维护,比如之前Facebook提供的 Three20 框架,在当时也是神器,但是后来不更新了,也就没什么人用了。因此我感觉学习一个框架,还是有必要了解它的编程思想。

先简单介绍下目前咱们已知的编程思想:

响应式编程思想

响应式编程思想:不需要考虑调用顺序,只需要知道考虑结果,类似于蝴蝶效应,产生一个事件,会影响很多东西,这些事件像流一样的传播出去,然后影响结果,借用面向对象的一句话,万物皆是流。

代表:KVO

链式编程思想

链式编程 是将多个操作(多行代码)通过点号(.)链接在一起成为一句代码,使代码可读性好。如:

make.add(1).add(2).sub(5).muilt(-4).divide(4);

特点:方法的返回值是block,block必须有返回值(本身对象),block参数(需要操作的值)

代表:masonry框架。

实现:模仿masonry,写一个加法计算器,练习链式编程思想。

NSObject+Caculator.h

# import <Foundation/Foundation.h>

@class CaculatorMaker;

@interface NSObject (Caculator)

// 计算
+ (int)makeCaculators:(void (^)(CaculatorMaker *))block;

@end

NSObject+Caculator.m

@implementation NSObject (Caculator)

+ (int)makeCaculators:(void (^)(CaculatorMaker *))block {

    CaculatorMaker *mgr = [[CaculatorMaker alloc] init];

    block(mgr);

    return (mgr.result);
}

@end

CaculatorMaker.h

# import <Foundation/Foundation.h>

@class CaculatorMaker;

typedef CaculatorMaker *(^CasulatorBlock)(int);

@interface CaculatorMaker : NSObject

@property (nonatomic, assign) int result;

// 算数方法
- (CaculatorMaker *(^)(int))add;
- (CasulatorBlock)sub;
- (CasulatorBlock)muilt;
- (CasulatorBlock)divide;


@end

CaculatorMaker.m

# import "CaculatorMaker.h"

@implementation CaculatorMaker

- (CaculatorMaker *(^)(int))add {

    return ^CaculatorMaker *(int value) {

        _result += value;

        return self;
    };
}

- (CasulatorBlock)sub {

    return ^CaculatorMaker *(int value) {

        _result -= value;

        return self;
    };
}

- (CasulatorBlock)muilt {

    return ^CaculatorMaker *(int value) {

        _result *= value;

        return self;
    };
}

- (CasulatorBlock)divide {

    return ^CaculatorMaker *(int value) {

        _result /= value;

        return self;
    };
}

@end

使用:

int result = [NSObject makeCaculators:^(CaculatorMaker *make) {

        // ( 1 + 2 - 5 ) * (-4) / 4
        make.add(1).add(2).sub(5).muilt(-4).divide(4);

    }];

    NSLog(@"%d", result);

函数式编程思想

函数式编程思想:是把操作尽量写成一系列嵌套的函数或者方法调用。

特点:每个方法必须有返回值(本身对象),把函数或者Block当做参数,block参数(需要操作的值)block返回值(操作结果)

代表ReactiveCocoa

实现:用函数式编程实现,写一个加法计算器,并且加法计算器自带判断是否等于某个值.

Calculator *caculator = [[Calculator alloc] init];

BOOL isqule = [[[caculator caculator:^int(int result) {

    result += 2;
    result *= 5;
    return result;

}] equle:^BOOL(int result) {

    return result == 10;

}] isEqule];

NSLog(@"%d", isqule);

Calculator.h

#import <Foundation/Foundation.h>

@interface Calculator : NSObject

@property (nonatomic, assign) BOOL isEqule;

@property (nonatomic, assign) int result;

- (Calculator *)caculator:(int (^)(int result))caculator;

- (Calculator *)equle:(BOOL (^)(int result))operation;

@end

Calculator.m

#import "Calculator.h"

@implementation Calculator

- (Calculator *)caculator:(int (^)(int))caculator {

    _result = caculator(_result);

    return self;

}


- (Calculator *)equle:(BOOL (^)(int))operation {

    _isEqule = operation(_result);

    return self;
}

@end

ReactiveCocoa 结合了这两种种编程风格:

  • 函数式编程(Functional Programming)

  • 响应式编程(Reactive Programming)

所以,你可能听说过 ReactiveCocoa 被描述为函数响应式编程(FRP)框架。

以后使用RAC解决问题,就不需要考虑调用顺序,直接考虑结果,把每一次操作都写成一系列嵌套的方法中,使代码高聚合,方便管理。


ReactiveCocoa的GitHub地址

Objective-C

ReactiveCocoa 2.5版本以后改用了Swift,所以Objective-C项目需要导入2.5版本

CocoaPods集成:

platform :ios, '8.0'

target 'YouProjectName' do

use_frameworks!
pod 'ReactiveCocoa', '~> 2.5'

end

PS:新版本的CocoaPods需要加入

target 'YouProjectName' do 
... 
end

这句话来限定项目,否则导入失败。

Swift

Swift项目导入2.5后的版本

platform :ios, '8.0'

target 'YouProjectName' do

use_frameworks!
pod 'ReactiveCocoa'

end

使用时在全局头文件导入头文件即可

PrefixHeader.pch

#ifndef PrefixHeader_pch
#define PrefixHeader_pch

#import <ReactiveCocoa/ReactiveCocoa.h>

#endif

RACSiganl 信号类

信号类,一般表示将来有数据传递,只要有数据改变,信号内部接收到数据,就会马上发出数据。

注意:

  • 信号类(RACSiganl),只是表示当数据改变时,信号内部会发出数据,它本身不具备发送信号的能力,而是交给内部一个订阅者去发出。
  • 默认一个信号都是冷信号,也就是值改变了,也不会触发,只有订阅了这个信号,这个信号才会变为热信号,值改变了才会触发。
  • 如何订阅信号:调用信号RACSignal的subscribeNext就能订阅
  • 订阅多次, 发送的闭包会多次执行,可以用RACMulticastConnection 避免

使用:

// RACSignal使用步骤:
    // 1.创建信号 + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
    // 2.订阅信号,才会激活信号. - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
    // 3.发送信号 - (void)sendNext:(id)value


    // RACSignal底层实现:
    // 1.创建信号,首先把didSubscribe保存到信号中,还不会触发。
    // 2.当信号被订阅,也就是调用signal的subscribeNext:nextBlock
    // 2.2 subscribeNext内部会创建订阅者subscriber,并且把nextBlock保存到subscriber中。
    // 2.1 subscribeNext内部会调用siganl的didSubscribe
    // 3.siganl的didSubscribe中调用[subscriber sendNext:@1];
    // 3.1 sendNext底层其实就是执行subscriber的nextBlock

    // 1.创建信号
    RACSignal *siganl = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        // block调用时刻:每当有订阅者订阅信号,就会调用block。

        // 2.发送信号
        [subscriber sendNext:@1];

        // 如果不在发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅信号。
        [subscriber sendCompleted];

        return [RACDisposable disposableWithBlock:^{

            // block调用时刻:当信号发送完成或者发送错误,就会自动执行这个block,取消订阅信号。

            // 执行完Block后,当前信号就不在被订阅了。

            NSLog(@"信号被销毁");

        }];
    }];

    // 3.订阅信号,才会激活信号.
    [siganl subscribeNext:^(id x) {
        // block调用时刻:每当有信号发出数据,就会调用block.
        NSLog(@"接收到数据:%@",x);
    }];

RACSubscriber

表示订阅者的意思,用于发送信号,这是一个协议,不是一个类,只要遵守这个协议,并且实现方法才能成为订阅者。通过create创建的信号,都有一个订阅者,帮助他发送数据。

RACDisposable

用于取消订阅或者清理资源,当信号发送完成或者发送错误的时候,就会自动触发它。

使用场景:不想监听某个信号时,可以通过它主动取消订阅信号。

RACSubject

RACSubject:信号提供者,自己可以充当信号,又能发送信号。

使用场景:通常用来代替代理,有了它,就不必要定义代理了。

RACReplaySubject

重复提供信号类,RACSubject的子类。

RACReplaySubjectRACSubject区别:

RACReplaySubject可以先发送信号,在订阅信号,RACSubject就不可以。

使用场景一:如果一个信号每被订阅一次,就需要把之前的值重复发送一遍,使用重复提供信号类。

使用场景二:可以设置capacity数量来限制缓存的value的数量,即只缓充最新的几个值。

ACSubjectRACReplaySubject 简单使用:

ACSubject

 // RACSubject使用步骤
// 1.创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
// 2.订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
// 3.发送信号 sendNext:(id)value

// RACSubject:底层实现和RACSignal不一样。
// 1.调用subscribeNext订阅信号,只是把订阅者保存起来,并且订阅者的nextBlock已经赋值了。
// 2.调用sendNext发送信号,遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。


// 1. 创建信号
RACSubject *subject = [RACSubject subject];

// 2.订阅信号
[subject subscribeNext:^(id x) {

    // block调用时机:当信号发出新值,就会调用
    NSLog(@"收到信号");

}];

// 3.发送信号
NSLog(@"发送信号");
[subject sendNext:@"1"];


 // RACReplaySubject使用步骤:
// 1.创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
// 2.可以先订阅信号,也可以先发送信号。
// 2.1 订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
// 2.2 发送信号 sendNext:(id)value

// RACReplaySubject:底层实现和RACSubject不一样。
// 1.调用sendNext发送信号,把值保存起来,然后遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。
// 2.调用subscribeNext订阅信号,遍历保存的所有值,一个一个调用订阅者的nextBlock

// 如果想当一个信号被订阅,就重复播放之前所有值,需要先发送信号,在订阅信号。
// 也就是先保存值,在订阅值。


// 1.创建信号
RACReplaySubject *replaySubject = [RACReplaySubject subject];



// 3.先订阅信号
[replaySubject subscribeNext:^(id x) {

    NSLog(@"第一个订阅者接受到的数据%@", x);
}];

// 2.发送信号
[replaySubject sendNext:@1];
[replaySubject sendNext:@2];

// 后订阅信号
[replaySubject subscribeNext:^(id x) {

    NSLog(@"第二个订阅者接收到的数据%@",x);
}];

RACSubject替换代理(与block类似)

// 需求:
    // 1.给当前控制器添加一个按钮,modal到另一个控制器界面
    // 2.另一个控制器view中有个按钮,点击按钮,通知当前控制器

步骤一:在第二个控制器.h,添加一个RACSubject代替代理。
@interface TwoViewController : UIViewController

@property (nonatomic, strong) RACSubject *delegateSignal;

@end

步骤二:监听第二个控制器按钮点击
@implementation TwoViewController
- (IBAction)notice:(id)sender {
    // 通知第一个控制器,告诉它,按钮被点了

     // 通知代理
     // 判断代理信号是否有值
    if (self.delegateSignal) {
        // 有值,才需要通知
        [self.delegateSignal sendNext:nil];
    }
}
@end

步骤三:在第一个控制器中,监听跳转按钮,给第二个控制器的代理信号赋值,并且监听.
@implementation OneViewController 
- (IBAction)btnClick:(id)sender {

    // 创建第二个控制器
    TwoViewController *twoVc = [[TwoViewController alloc] init];

    // 设置代理信号
    twoVc.delegateSignal = [RACSubject subject];

    // 订阅代理信号
    [twoVc.delegateSignal subscribeNext:^(id x) {

        NSLog(@"点击了通知按钮 %@", x);
    }];

    // 跳转到第二个控制器
    [self presentViewController:twoVc animated:YES completion:@"hi"];

}
@end

RACTuple

元组类,类似NSArray,用来包装值.(@[key, value])

RACSequence

RAC中的集合类,用于代替NSArray,NSDictionary,可以使用它来快速遍历数组和字典。

使用场景:字典转模型

// 1.遍历数组
NSArray *numbers = @[@1,@2,@3,@4];

// 这里其实是三步
// 第一步: 把数组转换成集合RACSequence numbers.rac_sequence
// 第二步: 把集合RACSequence转换RACSignal信号类,numbers.rac_sequence.signal
// 第三步: 订阅信号,激活信号,会自动把集合中的所有值,遍历出来。

[numbers.rac_sequence.signal subscribeNext:^(id x) {

    NSLog(@"%@", x);
}];



// 2.遍历字典,遍历出来的键值对 都会包装成 RACTuple(元组对象) @[key, value]
NSDictionary *dic = @{@"name": @"BYqiu", @"age": @18};

[dic.rac_sequence.signal subscribeNext:^(RACTuple *x) {

    // 解元组包,会把元组的值,按顺序给参数里的变量赋值
    // 写法相当与
    // NSString *key = x[0];
    // NSString *value = x[1];
    RACTupleUnpack(NSString *key, NSString *value) = x;

    NSLog(@"key:%@, value:%@", key, value);

}];

// 3.字典转模型

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"flags.plist" ofType:nil];

NSArray *dicArray = [NSArray arrayWithContentsOfFile:filePath];

NSMutableArray *items = [NSMutableArray array];

// OC写法
for (NSDictionary *dic in dicArray) {

    //FlagItem *item = [FlagItem flagWithDict:dict];
    //[items addObject:item];
}


// RAC写法
[dicArray.rac_sequence.signal subscribeNext:^(id x) {
    // 利用RAC遍历, x:字典

    //FlagItem *item = [FlagItem flagWithDict:x];
    //[items addObject:item];
}];

// RAC高级用法(函数式编程)
NSArray *flags = [[dicArray.rac_sequence map:^id(id value) {

    return  [FlagItem flagWithDict:value];

}] array];

RACCommand

RAC中用于处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程。

一、RACCommand使用步骤:

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

二、RACCommand使用注意:

  1. signalBlock必须要返回一个信号,不能传nil.
  2. 如果不想要传递信号,直接创建空的信号[RACSignal empty];
  3. RACCommand中信号如果数据传递完,必须调用[subscriber sendCompleted],这时命令才会执行完毕,否则永远处于执行中。
  4. RACCommand需要被强引用,否则接收不到RACCommand中的信号,因此RACCommand中的信号是延迟发送的。

三、RACCommand设计思想:

内部signalBlock为什么要返回一个信号,这个信号有什么用。

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

四、如何拿到RACCommand中返回信号发出的数据。

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

五、监听当前命令是否正在执行executing

六、使用场景,监听按钮点击,网络请求

使用:

// 1.创建命令
    RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
        NSLog(@"执行命令");

        // 返回空信号
        //return [RACSignal empty];

        // 2.创建信号 传递数据
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

            [subscriber sendNext:@"请求数据"];

            // 注意:数据传递完,最好调用sendCompleted,这时命令才执行完毕
            [subscriber sendCompleted];

            return nil;
        }];
    }];

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

    // 3.订阅RACCommand的信号
    [command.executionSignals subscribeNext:^(id x) {
        [x subscribeNext:^(id x) {

            NSLog(@"订阅RACCommand的信号: %@", x);
        }];
    }];

    // RAC高级用法
    // switchToLatest:用于signal of signals,获取signal of signals发出的最新信号,也就是可以直接拿到RACCommand中的信号
    [command.executionSignals.switchToLatest subscribeNext:^(id x) {

        NSLog(@"RAC高级用法: %@", x);
    }];

    // 4.监听命令是否执行完毕,默认会来一次,可以直接跳过,skip表示跳过第一次信号。
    [[command.executing skip:1] subscribeNext:^(id x) {

        if ([x boolValue] == YES) {

            // 正在执行
            NSLog(@"正在执行");

        } else {

            // 执行完毕
            NSLog(@"执行完成");
        }
    }];

    // 5.执行命名
    [self.command execute:@1];

RACMulticastConnection

用于当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的block,造成副作用,可以使用这个类处理。

注意:RACMulticastConnection通过RACSignal的-publish或者-muticast:方法创建.

RACMulticastConnection使用步骤:

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

RACMulticastConnection底层原理:

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

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

解决:使用RACMulticastConnection就能解决.

问题:每次订阅一次都会发送请求

// 创建请求信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    NSLog(@"发送请求");
    [subscriber sendNext:@1];

    return nil;
}];

// 订阅信号
[signal subscribeNext:^(id x) {

    NSLog(@"接受数据: %@", x);
}];

// 再次订阅信号,会再次执行发送请求,也就是每次订阅都会发送一次请求
[signal subscribeNext:^(id x) {

    NSLog(@"接受数据: %@", x);
}];

输出:

2016-12-28 11:37:04.397 ReactiveCacoa[1505:340573] 发送请求
2016-12-28 11:37:04.398 ReactiveCacoa[1505:340573] 接受数据: 1
2016-12-28 11:37:04.398 ReactiveCacoa[1505:340573] 发送请求
2016-12-28 11:37:04.398 ReactiveCacoa[1505:340573] 接受数据: 1

可以发现每次订阅都会重新发送请求.

下面我们使用RACMulticastConnection:

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

    NSLog(@"发送请求");
    [subscriber sendNext:@1];

    return nil;
}];

// 创建连接
RACMulticastConnection *connect = [signal publish];

// 订阅信号
// 注意:订阅信号,也不能激活信号,只是保存订阅者到数组,必须通过连接,当调用连接,就会一次性调用所有订阅者的SendNext
[connect.signal subscribeNext:^(id x) {

    NSLog(@"订阅者1信号: %@", x);
}];

[connect.signal subscribeNext:^(id x) {

    NSLog(@"订阅者2信号: %@", x);
}];

// 连接、激活信号
[connect connect];

输出:

2016-12-28 11:37:04.399 ReactiveCacoa[1505:340573] 发送请求
2016-12-28 11:37:04.399 ReactiveCacoa[1505:340573] 订阅者1信号: 1
2016-12-28 11:37:04.399 ReactiveCacoa[1505:340573] 订阅者2信号: 1

RACScheduler

RAC中的队列,用GCD封装的。

RACUnit

表⽰stream不包含有意义的值,也就是看到这个,可以直接理解为nil.

RACEven

把数据包装成信号事件(signal event)。它主要通过RACSignal的-materialize来使用,然并卵。

  1. 替换代理
  2. 替换KVO
  3. 监听事件
  4. 替换通知
  5. 监听文本框文字改变
  6. 统一处理多个网络请求

替换代理:

rac_signalForSelector:

rac_signalForSelector: 直接监听 Selector 事件的调用

应用场景:监听 RedViewController 中按钮的点击事件 btnTap:

跳转到RedViewController前,先使用rac_signalForSelector订阅rvc中的 btnTap: 点击事件

// 使用segue跳转
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
- 
    if ([segue.identifier isEqualToString:@"goRedVC"]) {

        RedViewController *rvc = segue.destinationViewController;

        // 订阅rvc中的 btnTap: 点击事件
        [[rvc rac_signalForSelector:@selector(btnTap:)] subscribeNext:^(id x) {

            NSLog(@"RedVC btnTap!");
        }];
    }
}

RedViewController.m 中的按钮事件

- (IBAction)btnTap:(id)sender {

    NSLog(@"!");
}

替换KVO

rac_valuesForKeyPath:

// KVO
// 监听 slider 的 value 变化
[[self.slider rac_valuesForKeyPath:@"value" observer:nil] subscribeNext:^(id x) {

    NSLog(@"slider value Change:%@", x);
}];

替换通知

rac_addObserverForName

// 原生的订阅通知
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(userDidChange:)
                                             name:kTTCurrentUserLoggedOffNotification
                                           object:nil];

// 使用RAC订阅通知 ,takeUntil限定信号的声明周期                                  
[[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil]
  takeUntil:[self rac_willDeallocSignal]]
  subscribeNext:^(id x) {
     NSLog(@"Notification received");
}];

监听事件

rac_signalForControlEvents:

// 监听 btn 的 UIControlEventTouchUpInside 点击事件
[[self.btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {

    NSLog(@"btnTap");
}];

监听 textField 文字变化

rac_textSignal

[[self.textField rac_textSignal] subscribeNext:^(id x) {

        NSLog(@"textField change: %@", x);
}];

统一处理多个网络请求

rac_liftSelector:

- (void)viewDidLoad {
    [super viewDidLoad];

        // 处理多个请求都返回结果的时候,统一处理
    // 如同时进行多个网络请求,每个请求都正确返回时,再去刷新页面

    RACSignal *signalOne = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        // 网络请求1
        // ...

        // 返回成功
        [subscriber sendNext:@"网络请求1 data"];

        return nil;
    }];

    RACSignal *signalTwo = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        // 网络请求2
        // ...

        // 返回成功
        [subscriber sendNext:@"网络请求2 data"];

        return nil;
    }];

    [self rac_liftSelector:@selector(updateWithR1:R2:) withSignalsFromArray:@[signalOne, signalTwo]];

}

// 更新界面
- (void)updateWithR1:(id)r1 R2:(id)r2 {

    NSLog(@"R1:%@, R2:%@ 完成!", r1, r2);

}

注意

  • 替换KVO监听文本框文字改变 方法在创建监听方法时就会执行一次。

    2016-12-28 16:53:50.746 ReactiveCacoa[4956:1246592] slider value Change:0.5
    2016-12-28 16:53:50.748 ReactiveCacoa[4956:1246592] textField change:

  • 使用rac_liftSelector@selector(updateWithR1:R2:) 中的方 参数个数 要与 signal个数 相同,否则会被断言Crash!

ReactiveObjC官方文档翻译

From: http://www.jianshu.com/p/eae223df2a19

此为ReactiveObjC文档翻译,gitHub tag:2.12 date:2017-03-15 QQ: 809199658.
原文地址:ReactiveCocoa/ReactiveObjC(四级险过翻译,请谅解)

提示:本文是使用老版本ReactiveCocoa - Objective-C的介绍,Objective-C ReactiveCocoa又名ReactiveObjC,如果希望使用新版(swift)的ReactiveCocoa,可以查看ReactiveCocoa或者ReactiveSwift.

ReactiveObjC (一般来说又叫做ReactiveCocoa或者RAC)是一个基于响应式编程的Objective-C的框架.使用它提供API用以创建和改变数据流.

如果你已经熟悉响应式编程或者了解基本的ReactiveObjC概念,你可以查看github上的其他文件来了解其框架的概述然后在实践中深入了解其使用

ReactiveObjC的文档非常完善,并且有非常丰富的介绍材料用以了解并使用RAC.

如果你想学习更多,我们推荐你下列的这些资源

  1. introduction
  2. when-to-use-reactiveobjc
  3. Framework Overview
  4. Basic Operators
  5. Header Documentation
  6. Stack Overflow之前问题的回答或者GitHub issues.
  7. 本文章的剩余部分
  8. Functional Reactive Programming on iOS (eBook)

如果你还有更多问题,请提交issue

ReactiveObjC是基于函数响应式编程,相对于修改或替换现有变量,RAC提供信号类(代表:RACSugnal)来捕捉现有和 未来(future???)的变量

通过链接,绑定以及响应信号,不需要监听来更新变量,使得程序更加简洁

例如,一个文本框被绑定后,相对于使用额外的代码去监听时间和更新文本框,使用block来重写RAC使得其更像KVO
-observeValueForKeyPath:ofObject:change:context:

信号类也可以像多线程一样使用,像并发式编程.这简化了包括在网络请求在内的多线程程序.

RAC的最大优点就是它提供了一个信号,统一了解决异步行为的方法,包括代理方法,target-action机制,通知,和KVO.

例1:

// 当self.username 改变时,控制台会输出他的新名字
//
// RACObserve(self, username)在文本改变的时候创建了发送当前姓名的一个新信号
// -subscribeNext: 在收到信号后执行block
[RACObserve(self, username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

相对于KVO,信号可以被链接到一起并一同执行

//只打印以'j'开头的名字
//
// 过滤器在第一个信号block被调用返回YES时才发送一个新值给下一个block
[[RACObserve(self, username)
    filter:^(NSString *newName) {
        return [newName hasPrefix:@"j"];
    }]
    subscribeNext:^(NSString *newName) {
        NSLog(@"%@", newName);
    }];

信号也可以用来获取状态,RAC可以将属性快速的转换成信号和操作来替代监听属性然后改变值得这种方式.

//创建一个单程绑定,这样当self.password和self.passwordConfirmation一样时self.createEnabled将会变成ture
//
// RAC() 宏可以使得绑定表示的更清楚
//
// +combineLatest:reduce: 获取一个信号数组,当其中任何一个改变时传递新值并调用这些blocks,并且返回block返回值的一个新RACSignal信号
//(returns a new RACSignal that sends the return value of that block as values.)
RAC(self, createEnabled) = [RACSignal
    combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ]
    reduce:^(NSString *password, NSString *passwordConfirm) {
        return @([passwordConfirm isEqualToString:password]);
    }];

信号可以在任何时间任何数据流上创建,不仅限于KVO,例如,可以用于按钮点击事件

// 在按钮点击时打印信息
// RACCommand 用以表示一个UI事件. 每一个信号可以表示一次点击, 例如,在点击事件时处理额外的事件
// -rac_command 是 NSButton 的额外方法 . 在点击时,按钮将自身发送给它自己.
self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
    NSLog(@"button was pressed!");
    return [RACSignal empty];
}];

或者异步网络请求

// 给按钮绑定一个登录事件请求网络
//
// 在登录命令执行的时候这个block将用以执行登录程序
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
    // 这个登录命令在网络请求结束后返回发送值得一个信号
    return [client logIn];
}];

// -executionSignals 返回一个包含上面block的信号,收到一次信号执行一次
[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
    // 登录成功调用
    [loginSignal subscribeCompleted:^{
        NSLog(@"Logged in successfully!");
    }];
}];

// 按钮点击时执行登录命令
self.loginButton.rac_command = self.loginCommand;

信号也可以在timer,UI事件,或者其他随时间改变的事情上

将多个信号链接在一起成为一组操作(a group of operations completes)可以使得更复杂的异步操作变得简单

// 在两个网络请求结束后打印信息
//
// +merge: takes an array of signals and returns a new RACSignal that passes
// through the values of all of the signals and completes when all of the
// signals complete.(太长了,求翻译)
//
// -subscribeCompleted: 在信号结束后调用
[[RACSignal
    merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
    subscribeCompleted:^{
        NSLog(@"They're both done!");
    }];

多个异步操作也可以将信号链接实现,不同于回调block,这个和并发操作的一般做法一致.

// 登录用户,服务器获取信息,获取本地缓存信息,之后显示全部信息
//
// hypothetical -logInUser 在登录完成后返回完成信号
//
// -flattenMap: 在信号发送后执行block, 返回一个合并所有block返回信号的新的RACSignal
[[[[client logInUser]
    flattenMap:^(User *user) {
       //返回本地缓存已读取完成的信号
        return [client loadCachedMessagesForUser:user];
    }]
    flattenMap:^(NSArray *messages) {
        // 返回信息已匹配的信号
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeNext:^(NSArray *newMessages) {
        NSLog(@"New messages: %@", newMessages);
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

RAC也可以轻松绑定一步操作的结果

// 在用户头像下载完成后可以轻松绑定self.imageView.image
//
//hypothetical -fetchUserWithUsername: 发送给用户的信号
//
// -deliverOn: 在其他队列上创建一个新的信号在本例中,
//它用来将任务放置在后台队列然后返回主队列中
//
RAC(self.imageView, image) = [[[[client
    fetchUserWithUsername:@"joshaber"]
    deliverOn:[RACScheduler scheduler]]
    map:^(User *user) {
        // 在后台下载头像
        return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
    }]
    // 此操作在主线程
    deliverOn:RACScheduler.mainThreadScheduler];

上面这些例子表明了RAC可以做的事情,但是很难再README大小的例子中展示为什么RAC很犀利,但是可以表明RAC怎么样让代码更清晰(吹牛中)….

如果要更多的代码示例,可以查看C-41或者GroceryList.其中所有的iOS APP都是用RAC来实现的,更多关于RAC的信息都可以在其中找到.

初看的时候,RAC非常抽象,而且很难了解什么时候使用来解决问题.
接下来的demo是RAC擅长的领域.

操作异步或者事件驱动数据源

许多的cocoa程序关注于程序对事件的响应或改变,处理的代码很可能变得非常复杂(就像意大利面条🍝一样),非常多的回调block和状态变量用来处理问题.
样式(Patterns)看起来是突出的那个,像UI的回调,网络回应,KVO,通知,其实他们都有许多一样的地方,RACSignal统一了不用的API调用方法,让他们用一致的方式被调用.

例如:

static void *ObservationContext = &ObservationContext;

- (void)viewDidLoad {
    [super viewDidLoad];

    [LoginManager.sharedManager addObserver:self forKeyPath:@"loggingIn" options:NSKeyValueObservingOptionInitial context:&ObservationContext];
    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(loggedOut:) name:UserDidLogOutNotification object:LoginManager.sharedManager];

    [self.usernameTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged];
    [self.passwordTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged];
    [self.logInButton addTarget:self action:@selector(logInPressed:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)dealloc {
    [LoginManager.sharedManager removeObserver:self forKeyPath:@"loggingIn" context:ObservationContext];
    [NSNotificationCenter.defaultCenter removeObserver:self];
}

- (void)updateLogInButton {
    BOOL textFieldsNonEmpty = self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0;
    BOOL readyToLogIn = !LoginManager.sharedManager.isLoggingIn && !self.loggedIn;
    self.logInButton.enabled = textFieldsNonEmpty && readyToLogIn;
}

- (IBAction)logInPressed:(UIButton *)sender {
    [[LoginManager sharedManager]
        logInWithUsername:self.usernameTextField.text
        password:self.passwordTextField.text
        success:^{
            self.loggedIn = YES;
        } failure:^(NSError *error) {
            [self presentError:error];
        }];
}

- (void)loggedOut:(NSNotification *)notification {
    self.loggedIn = NO;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == ObservationContext) {
        [self updateLogInButton];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

用RAC可以被这样实现

- (void)viewDidLoad {
    [super viewDidLoad];

    @weakify(self);
//将button的enable和accountText,passwordText,isLogin,logined绑定起来
    RAC(self.logInButton, enabled) = [RACSignal
        combineLatest:@[
            self.usernameTextField.rac_textSignal,
            self.passwordTextField.rac_textSignal,
            RACObserve(LoginManager.sharedManager, loggingIn),
            RACObserve(self, loggedIn)
        ] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) {
            return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue);
        }];
//loginbutton 的点击事件
    [[self.logInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) {
        @strongify(self);
//调起登录接口
        RACSignal *loginSignal = [LoginManager.sharedManager
            logInWithUsername:self.usernameTextField.text
            password:self.passwordTextField.text];
//登录接口回调
            [loginSignal subscribeError:^(NSError *error) {
                @strongify(self);
                [self presentError:error];
            } completed:^{
                @strongify(self);
                self.loggedIn = YES;
            }];
    }];

    RAC(self, loggedIn) = [[NSNotificationCenter.defaultCenter
        rac_addObserverForName:UserDidLogOutNotification object:nil]
        mapReplace:@NO];
}

链式依赖操作

网络请求中经常出现依赖关系,在上一个操作完成后才能进行下一个操作,像下面这样:

[client logInWithSuccess:^{
    [client loadCachedMessagesWithSuccess:^(NSArray *messages) {
        [client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) {
            NSLog(@"Fetched all messages.");
        } failure:^(NSError *error) {
            [self presentError:error];
        }];
    } failure:^(NSError *error) {
        [self presentError:error];
    }];
} failure:^(NSError *error) {
    [self presentError:error];
}];

使用RAC可以更简单

[[[[client logIn]
    then:^{
        return [client loadCachedMessages];
    }]
    flattenMap:^(NSArray *messages) {
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeError:^(NSError *error) {
        [self presentError:error];
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

独立并行功能

在并行的线程处理独立数据并把处理好的数据合并在一起在cocoa中是非常重要的事情,而且很容易引起同步问题.

__block NSArray *databaseObjects;
__block NSArray *fileContents;

NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
NSBlockOperation *databaseOperation = [NSBlockOperation blockOperationWithBlock:^{
    databaseObjects = [databaseClient fetchObjectsMatchingPredicate:predicate];
}];

NSBlockOperation *filesOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSMutableArray *filesInProgress = [NSMutableArray array];
    for (NSString *path in files) {
        [filesInProgress addObject:[NSData dataWithContentsOfFile:path]];
    }

    fileContents = [filesInProgress copy];
}];

NSBlockOperation *finishOperation = [NSBlockOperation blockOperationWithBlock:^{
    [self finishProcessingDatabaseObjects:databaseObjects fileContents:fileContents];
    NSLog(@"Done processing");
}];

[finishOperation addDependency:databaseOperation];
[finishOperation addDependency:filesOperation];
[backgroundQueue addOperation:databaseOperation];
[backgroundQueue addOperation:filesOperation];
[backgroundQueue addOperation:finishOperation];

使用RAC可以简化成如下代码

RACSignal *databaseSignal = [[databaseClient
    fetchObjectsMatchingPredicate:predicate]
    subscribeOn:[RACScheduler scheduler]];

RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
    NSMutableArray *filesInProgress = [NSMutableArray array];
    for (NSString *path in files) {
        [filesInProgress addObject:[NSData dataWithContentsOfFile:path]];
    }

    [subscriber sendNext:[filesInProgress copy]];
    [subscriber sendCompleted];
}];

[[RACSignal
    combineLatest:@[ databaseSignal, fileSignal ]
    reduce:^ id (NSArray *databaseObjects, NSArray *fileContents) {
        [self finishProcessingDatabaseObjects:databaseObjects fileContents:fileContents];
        return nil;
    }]
    subscribeCompleted:^{
        NSLog(@"Done processing");
    }];

简化集合的变形

高序列化功能像map,filter,fold/reduce也可能在fondation框架中缺失或者导致loop-focused崩溃.

//在遍历中添加数据导致无限循环
NSMutableArray *results = [NSMutableArray array];
for (NSString *str in strings) {
    if (str.length < 2) {
        continue;
    }

    NSString *newString = [str stringByAppendingString:@"foobar"];
    [results addObject:newString];
}

RAC中[RACSequence][17]允许在遍历中添加值.

[17]: https://link.jianshu.com?t=https://github.com/ReactiveCocoa/ReactiveObjC/blob/master/ReactiveObjC/RACSequence.h

RACSequence results = [[strings.rac_sequence
filter:^ BOOL (NSString
str) {
return str.length >= 2;
}]
map:^(NSString *str) {
return [str stringByAppendingString:@”foobar”];
}];

系统要求

OS X 10.8+ 及iOS8.0+

导入ReactiveObjC

导入ReactiveObjC你可以

  1. 添加ReactiveObjC仓库到你的程序仓库中
  2. 在你的ReactiveObjC文件夹中运行
    git submodule update --init --recursive
  3. ReactiveObjC.xcodeproj拖入你的Xcode项目中
  4. 在”Build Phases”中添加RAC到”Link Binary With Libraries”中
  5. 添加ReactiveObjC.frameworkRAC也必须添加到任何”Copy Frameworks”build phase中.如果你还没创建,简单的添加一个”Copy Files” build phase指向(target)”Frameworks”位置
  6. 添加"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include" $(inherited)到”Header Search Paths” build setting 中
  7. 如果是iOS,则需要在”Other Linker Flags” build setting添加-ObjC

四级渣翻译,如果有什么意见或者改善的地方可以留言或者通过QQ联系我~

dispatch_semaphore和NSOperationQueue并发 (GCD 等)

From: http://www.liuchendi.com/2015/11/10/iOS/2_dispatch_semaphore%E5%92%8CNSOperationQueue%E5%B9%B6%E5%8F%91/

并发:同一个时间内运行多个任务。又叫共行性,是指处理多个同时性活动的能力。
并行:是指两个并发的任务同时发生。

并发不一定并行,并发不一定要同时发生。

举个例子:

有两个快递分别要送到目的地,有以下两种方案:

(1)一个快递员分别把两个快递送到对应的目的地。(并发)

(2)两个快递员同时送一个快递到目的地。(并行)

在iOS中,经常可以看见有这样的需求,就是一个方法要等另外一个方法执行完毕再做相对应的处理,比如说一些网络请求,需要根据上一个请求的返回值做相对应的处理再执行第二个请求,所以我们不能让两个请求同时去请求网络。下面就记录以下通过GCD和NSOperationQueue来控制并发。

##dispatch_semaphore

信号量是一个整型值并且具有初始计数值,信号量通常支持两个操作:通知和等待。当信号被通知的时候计数值会增加,当信号量在线程上等待的时候,必要的情况下线程会被阻塞掉,直至信号被通知时计数值大于0,然后线程会减少这个计数继续工作。

GCD中又3个信号量有关的操作:

dispatch_semaphore_create    信号量创  

dispatch_semaphore_signal    发送通知  

dispatch_semaphore_wait     信号量等待  



__block dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_queue_create("testBlock", NULL);
dispatch_async(queue, ^{      
    for (int i = 0 ; i < 100; i++) {
        NSLog(@"i的值是:%d",i);
    }
    dispatch_semaphore_signal(sem);
});   
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
for (int j = 0; j < 10; j ++) {
    NSLog(@"j的值是:%d",j);
}  

运行结果是:

##NSOperationQueue
在不添加依赖的情况下:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 10;

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{

    for (int i = 0; i < 1000; i++) {

        NSLog(@"执行并发队列1:%d",i);
    }
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^(){

    for (int i = 0; i < 1500; i++) {

        NSLog(@"执行并发队列2:%d",i);
    }
}];


[queue addOperation:operation1];
[queue addOperation:operation2];

运行结果是:

添加依赖控制后:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 10;

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{

    for (int i = 0; i < 1000; i++) {

        NSLog(@"执行并发队列1:%d",i);
    }
}];

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^(){

    for (int i = 0; i < 1500; i++) {

        NSLog(@"执行并发队列2:%d",i);
    }
}];



[operation1 addDependency:operation2];   

[queue addOperation:operation1];
[queue addOperation:operation2];

运行结果是:


From: http://blog.csdn.net/Cloudox_/article/details/71107179

对计算机了解的都会知道信号量的作用,当我们多个线程要访问同一个资源的时候,往往会设置一个信号量,当信号量大于0的时候,新的线程可以去操作这个资源,操作时信号量-1,操作完后信号量+1,当信号量等于0的时候,必须等待,所以通过控制信号量,我们可以控制能够同时进行的并发数。

在网络请求的开发中,经常会遇到两种情况,一种是我在一个界面需要同时请求多种数据,比如列表数据、广告数据等,全部请求到后再一起刷新界面。另一种是我的请求必须满足一定顺序,比如必须先请求个人信息,然后根据个人信息请求相关内容。这些要求对于普通的操作是可以做到并发控制和依赖操作的,但是对于网络请求这种需要时间的请求来说,效果往往与预期的不一样,这时候就需要用信号量来做一个控制。

信号量是一个整数,在创建的时候会有一个初始值,这个初始值往往代表我要控制的同时操作的并发数。在操作中,对信号量会有两种操作:信号通知与等待。信号通知时,信号量会+1,等待时,如果信号量大于0,则会将信号量-1,否则,会等待直到信号量大于0。什么时候会大于零呢?往往是在之前某个操作结束后,我们发出信号通知,让信号量+1。

说完概念,我们来看看GCD中的三个信号量操作:

  • dispatch_semaphore_create:创建一个信号量(semaphore)
  • dispatch_semaphore_signal:信号通知,即让信号量+1
  • dispatch_semaphore_wait:等待,直到信号量大于0时,即可操作,同时将信号量-1

在使用的时候,往往会创建一个信号量,然后进行多个操作,每次操作都等待信号量大于0再操作,同时信号昂-1,操作完后将信号量+1,类似下面这个过程:

dispatch_semaphore_t sema = dispatch_semaphore_create(5);
for (100次循环操作) {
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        dispatch_semaphore_signal(sema);
    });
}

上面代码表示我要操作100次,但是控制允许同时并发的操作最多只有5次,当并发量达到5后,信号量就减小到0了,这时候wait操作会起作用,DISPATCH_TIME_FOREVER表示会永远等待,一直等到信号量大于0,也就是有操作完成了,将信号量+1了,这时候才可以结束等待,进行操作,并且将信号量-1,这样新的任务又要等待。

假设我们一个页面需要同时进行多个请求,他们之间倒是不要求顺序关系,但是要求等他们都请求完毕了再进行界面刷新或者其他什么操作。

这个需求我们一般可以用GCD的group和notify来做到:

dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    NSLog(@"Request_1");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    NSLog(@"Request_2");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    NSLog(@"Request_3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{

    NSLog(@"任务均完成,刷新界面");
});

notify的作用就是在group中的其他操作全部完成后,再操作自己的内容,所以我们会看到上面三个内容都打印出来后,才打印界面刷新的内容。

但是当将上面三个操作改成真实的网络操作后,这个简单的做法会变得无效,为什么呢?因为网络请求需要时间,而线程的执行并不会等待请求完成后才真正算作完成,而是只负责将请求发出去,线程就认为自己的任务算完成了,当三个请求都发送出去,就会执行notify中的内容,但请求结果返回的时间是不一定的,也就导致界面都刷新了,请求才返回,这就是无效的。

要解决这个问题,我们就要用到上面说的信号量来操作了。

在每个请求开始之前,我们创建一个信号量,初始为0,在请求操作之后,我们设一个dispatch_semaphore_wait,在请求到结果之后,再将信号量+1,也即是dispatch_semaphore_signal。这样做的目的是保证在请求结果没有返回之前,一直让线程等待在那里,这样一个线程的任务一直在等待,就不会算作完成,notify的内容也就不会执行了,直到每个请求的结果都返回了,线程任务才能够结束,这时候notify也才能够执行。伪代码如下:

dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[网络请求:{
        成功:dispatch_semaphore_signal(sema);
        失败:dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

有时候我们需要按照顺序执行多次请求,比如先请求到用户信息,然后根据用户信息中的内容去请求相关的数据,这在平常的代码中直接按照顺序往下写代码就可以了,但这里因为涉及到多线程之间的关系,就叫做线程依赖。

线程依赖用GCD做比较麻烦,建议用NSOperationQueue做,可以更加方便的设置任务之间的依赖。

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    [self request_A];
}];


NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    [self request_B];
}];


[operation2 addDependency:operation1];


NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation2, operation1] waitUntilFinished:NO];

一般的多线程操作这样做是可以的,线程2会等待线程1完成后再执行。但是对于网络请求,问题又来了,同样,网络请求需要时间,线程发出请求后即认为任务完成了,并不会等待返回后的操作,这就失去了意义。

要解决这个问题,还是用信号量来控制,其实是一个道理,代码也是一样的,在一个任务操作中:

dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[网络请求:{
        成功:dispatch_semaphore_signal(sema);
        失败:dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

/// 注意上述网络请求在主线程返回数据, 上述代码在主线程执行会死锁

还是去等待请求返回后,才让任务结束。而依赖关系则通过NSOperationQueue来实现。

其实归根结底,中心思想就是通过信号量,来控制线程任务什么时候算作结束,如果不用信号量,请求发出后即认为任务完成,而网络请求又要不同时间,所以会打乱顺序。因此用一个信号量来控制在单个线程操作内,必须等待请求返回,自己要执行的操作完成后,才将信号量+1,这时候一直处于等待的代码也得以执行通过,任务才算作完成。

通过这个方法,就可以解决由于网络请求耗时特性而带来的一些意想不到的多线程处理的问题。


版权所有:http://blog.csdn.net/cloudox_

参考资料:
1、http://www.cocoachina.com/ios/20170428/19150.html
2、http://blog.csdn.net/fhbystudy/article/details/25918451


From: https://www.jianshu.com/p/3d1eab375295

dispatch group 可以把并行执行的多个任务合成一组,于是调用者就可以知道这些任务何时才能全部执行完毕。例如,可以把一系列操作文件的任务或者网络请求的任务添加到 dispatch group 中,等全部完成这些耗时的任务之后再进行下一步操作。
创建 dispatch group 的方式如下:

dispatch_group_t dispatch_group_create(void);

dispatch group 就是个简单的数据结构,这种结构彼此之间没有什么区别,它不像派发队列,后者还有个用来区别身份的标识符。

想把任务编组,有两种方法。第一种是用下面这个函数:

dispatch_group_async(dispatch_group_t group,
    dispatch_queue_t queue,
    dispatch_block_t block);

它是普通 dispatch_async 函数的变体。group 参数用于表示执行的块所归属的组,queue 表示执行任务的队列。所以 dispatch_group_async 可以方便的向 dispatch group 中添加不同队列的不同任务。此函数中的 queue 既可以时串行队列,又可以时并行队列。然而,如果所有任务都排在同一个串行队列里,那么 dispatch group 的用处就不大了,因为只需要在提交完全部任务之后再提交一个块即可。

第二种能够指定任务所属的 dispatch group 的方法是使用下面这一对函数:

void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);

前者能够使 dispatch group 里正要执行的任务数递增,而后者则使之递减。所以,调用完 dispatch_group_enter 以后,必须有与之对应的 dispatch_group_leave 才行。如果调用 enter 之后,没有相应的 leave 操作,那么这一组任务就永远执行不完。在使用时,可以在向队列中添加任务时调用 enter,在任务执行完成之后合适的地方调用 leave

当像 group 中添加完所有的任务之后,有两种方式可以等待任务完成。第一种方法是使用下面的函数:

long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

此函数有两个参数,group 是要等待的线程组,timeout 表示要等待的时间。timeout 参数表示函数在等待 dispatch group 执行完毕时,应该阻塞多久。如果执行 dispatch group 所需的时间小于 timeout,则返回 0,否则返回非 0 值。此参数也可以取常量 DISPATCH_TIME_FOREVER,这表示函数会一直等待 dispatch group 执行完,而不会超时。注意:dispatch_group_wait 是同步的,会阻塞当前线程,所以不能放在主线程执行。

第二种方式时使用下面函数:

void dispatch_group_notify(dispatch_group_t group,
        dispatch_queue_t queue,
        dispatch_block_t block);

dispatch_group_wait 不同的是该函数不会阻塞当前线程。开发着可以向此函数传入 block,等 dispatch group 执行完毕之后,block 会在指定的线程上运行。

使用 dispatch_group_wait 的例子:

  • object-c

    NSLog(@”— 开始设置任务 —-“);
    // 因为 dispatch_group_wait 会阻塞线程,所以创建一个新的线程,用来完成任务
    // 同时用异步的方式向新线程(tasksQueue)中添加任务
    dispatch_queue_t tasksQueue = dispatch_queue_create(“tasksQueue”, DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(tasksQueue, ^{

    // 真正用来完成任务的线程
    dispatch_queue_t performTasksQueue = dispatch_queue_create("performTasksQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    for (int i = 0; i < 3; i++) {
        // 入组之后的 block 会被 group 监听
        // 注意:dispatch_group_enter 一定和 dispatch_group_leave 要配对出现
        dispatch_group_enter(group);
        dispatch_async(performTasksQueue, ^{
            NSLog(@"开始第 %zd 项任务", i);
            [NSThread sleepForTimeInterval:(3 - i)];
            dispatch_group_leave(group);
            NSLog(@"完成第 %zd 项任务", i);
        });
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"全部任务完成");
    });
    

    });
    NSLog(@”— 结束设置任务 —-“);

  • swift 3.0

    print(“— 开始设置任务 —-“);
    DispatchQueue.main.async {

    let group = DispatchGroup.init()
    let tasksQueue = DispatchQueue.init(label: "tasksQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
    for i in 0..<3 {
        group.enter()
        tasksQueue.async {
            print("开始第 \(i) 项任务")
            sleep(UInt32(3 - i))
            print("结束第 \(i) 项任务")
            group.leave()
        }
    }
    //
    let result = group.wait(timeout: DispatchTime.distantFuture)
    if result == .success {
        print("全部任务完成")
    } else {
        print("任务执行超时")
    }
    

    }
    print(“— 开始设置任务 —-“);

输出如下:

— 开始设置任务 —-
— 结束设置任务 —-
开始第 0 项任务
开始第 1 项任务
开始第 2 项任务
完成第 2 项任务
完成第 1 项任务
完成第 0 项任务
全部任务完成

swift 4 代码 组, 当然信号量也是能实现这样的功能的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//
let group = DispatchGroup()
// let quebook = DispatchQueue(label: "com.boosj.usersavedata")
let quebook = DispatchQueue(label: "com.boosj.usersavedata", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent, autoreleaseFrequency: .inherit, target: nil)
var isSaveUser = false
var isSaveImage = false
quebook.async(group: group){
isSaveUser = true
// 这个和下面的leave 配合使用, 会等待手动控制结束
group.enter()
NetTool.shared.saveUserMessage(message: self.viewModel.userMessageMode, complete: { (success, error) in
printLog("信息保存成功")
group.leave()
})
}
quebook.async(group: group){
isSaveImage = true
group.enter()
NetTool.shared.saveUserHeadImage(image: self.viewModel.userMessageMode.userImage!, complete: { (success, error) in
printLog("图片保存成功")
group.leave()
})
}
group.notify(queue: DispatchQueue.main){
// 执行完成
printLog("全部执行完毕")
}

iOS 事件处理(2)

接上面

具体例子

用自定义触摸事件来实现相关 UIGestureRecognizer。这一小节的详细代码可见Multitouch EventsListing 3-1Listing 3-7

处理点击手势

用 UITouch 的 tapCount 属性来判断是单击还是双击还是三击。最好是在 touchesEnded:withEvent: 方法里面做判断处理,因为它是用户手指离开 App 时才响应的,要确保它真的是个 tap 手势,而不是拖动啥的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *aTouch in touches) {
if (aTouch.tapCount >= 2) {
// The view responds to the tap
[self respondToDoubleTapGesture:aTouch];
}
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
}

处理滑动和拖动手势

滑动手势

从三个角度判断它是否是一个滑动手势

  • Did the user’s finger move far enough?
  • Did the finger move in a relatively straight line?
  • Did the finger move quickly enough to call it a swipe?

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#define HORIZ_SWIPE_DRAG_MIN 12
#define VERT_SWIPE_DRAG_MAX 4
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
// startTouchPosition is a property
self.startTouchPosition = [aTouch locationInView:self];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
CGPoint currentTouchPosition = [aTouch locationInView:self];
// Check if direction of touch is horizontal and long enough
if (fabsf(self.startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN &&
fabsf(self.startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX)
{
// If touch appears to be a swipe
if (self.startTouchPosition.x < currentTouchPosition.x) {
[self myProcessRightSwipe:touches withEvent:event];
} else {
[self myProcessLeftSwipe:touches withEvent:event];
}
self.startTouchPosition = CGPointZero;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
self.startTouchPosition = CGPointZero;
}

拖动手势

简单的一根手指拖动的相关代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *aTouch = [touches anyObject];
CGPoint loc = [aTouch locationInView:self];
CGPoint prevloc = [aTouch previousLocationInView:self];
CGRect myFrame = self.frame;
float deltaX = loc.x - prevloc.x;
float deltaY = loc.y - prevloc.y;
myFrame.origin.x += deltaX;
myFrame.origin.y += deltaY;
[self setFrame:myFrame];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
}

多点触摸

taps,drags,swipes 通常都只涉及了一个 touch,比较简单去跟踪。但是处理由多个 touches 组成的触摸事件时,比较有挑战性。需要去记录 touch 的所有相关属性,并且改变它的 state 等等。需要做到两点:

  • Set the view’s multipleTouchEnabled property to YES;将多点触摸属性置为 YES;
  • Use a Core Foundation dictionary object (CFDictionaryRef) to track the mutations of touches through their phases during the event;用 CFDictionaryRef 来跟着 UITouch,这里用 CFDictionaryRef 而不是 NSDictionary,因为 NSDictionary 会 copy 它的 key。而 UITouch 没有采取 NSCopying 协议。

Determining when the last touch in a multitouch sequence has ended,判断 multitouch sequence 里的最后一个 touch 是否结束,可以用下面的代码

1
2
3
4
5
6
7
8
9
10
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
if ([touches count] == [[event touchesForView:self] count]) {
// Last finger has lifted
}
}

指定自定义的触摸事件行为

通过改变一些属性去改变事件流的处理。

多个 touch 的分发传递(multipleTouchEnabled)

Turn on delivery of multiple touches. 默认值为NO,意味着只会接受触摸队列里面的第一个 touch,其他的会忽略掉。所以,[touches anyObject]方法就只会返回一个对象。将属性 multipleTouchEnabled 设为 YES,则可以处理多个 touches。

限制事件只分发给一个 view(exclusiveTouch)

Restrict event delivery to a single view. 即只有一个 view 响应事件。默认情况下,view 的 exclusiveTouch 属性为 NO,这就意味着,一个 view 不会阻塞 window里的其他 view 去接受事件。如果将某个 view 的 exclusiveTouch 设为 YES,那么当它接受 touches 时,只会有它一个接收 touches 。这里举例说明了 exclusiveTouch 属性,A、B、C 3个 view 多点触摸的例子。

Apple Restricting event delivery with an exclusive-touch view

If the user touches inside A, it recognizes the touch. But if a user holds one finger inside view B and also touches inside view A, then view A does not receive the touch because it was not the only view tracking touches. Similarly, if a user holds one finger inside view A and also touches inside view B, then view B does not receive the touch because view A is the only view tracking touches. At any time, the user can still touch both B and C, and those views can track their touches simultaneously.

exclusiveTouch 这个属性比较傲娇,只有当设置它的为 YES 的 view 首先收到触摸事件时,它才能响应。

限制事件分发到 subviews 上 (hitTest:withEvent:)

Restrict event delivery to subviews. 重载 hitTest:withEvent: 方法返回自己 self。

关闭事件的分发

  • userInteractionEnabled 属性置为 NO;
  • hidden 属性置为 NO;
  • alpha 属性值 <= 0.01;

阶段性的关闭事件的分发

beginIgnoringInteractionEvents 方法关闭,endIgnoringInteractionEvents 方法开启。这个方法是 UIApplication 的,所以能做一些全局性的事情。

触摸事件的转发

你可以将一个事件转发给另外一个响应对象(响应链就是这样玩的嘛),当你使用这个技术的时候得小心,因为 UIKit 没有设计去接受不属于它们的事件。所以,你不要转发给 UIKit 框架的对象。如果你想要有条件的去转发事件给其他响应对象时,那么这些对象应该是 UIView 的实例,并且这些对象关心事件的转发,并且能够处理这些事件。原因如下:

For a responder object to handle a touch, the touch’s view property must hold a reference to the responder. 一个 responder 对象想要处理一个 touch ,那么 touch 的 view 属性必须持有这个 responder。

事件的转发经常需要去分析 touch 对象觉得它是否应该转发事件。这里有一些方法你可以采取去分析:

  • With an “overlay” view, such as a common superview, use hit-testing to intercept events for analysis prior to forwarding them to subviews.(使用 overlay view,例如公用的父视图,在转发到 subviews 之前拦截事件去分析)
  • Override sendEvent: in a custom subclass of UIWindow, analyze touches, and forward them to the appropriate responders.(UIWindow 的子类重载 sendEvent: 方法,将事件转发到合适的 responders)

重载 sendEvent: 方法可以监听 App 事件的接收。UIApplication 和 UIWindow 都是用这个方法来分发事件的,所以它就是事件进入 App 的管道一样。当你重载的时候,务必调用父类的实现,[super sendEvent:event]。在 control 和 gesture recognizer 的响应事件里面打断点,可以看到,事件走的 UIKit 开始传递都是先走的,[UIApplication sendEvent:]、[UIWindow sendEvent:],最终都是走的 [UIApplication sendAction:to:from:forEvent:]。

gesture recognizer handle flow

control handle flow

处理多点触摸事件的最佳实践

当处理 touch 和 motion 事件时,这里有一些值得推荐的技巧和模式:

  • 记得实现事件的取消方法。
  • 如果自定义的是 UIView、UIViewController、UIResponder的子类时,你应该实现所有的事件方法,即使里面没有做任何实现。但是不要在里面调用父类的实现。
  • 如果是其他 UIKit 的子类时,你没有必须实现所有的事件方法。但是,你必须得调用父类的实现。即 [super touchesBegan:touches withEvent:event] 。
  • 只转发事件给 UIView 的子类。并确保这些转发后的对象能够知道并且处理这些不属于它的事件。
  • 不要显示的通过 nextResponder 方法在响应链上发送事件。相反的,调用父类的实现,并且让 UIKit 去遍历处理。
  • 不要使用 round-to-integer 代码(即不要使用 integer 来处理 float),这样会丢失精度。
  • 如果在事件处理的时候需要创建持久对象,记得在 touchesCancelled:withEvent 和 touchesEnded:withEvent: 里面销毁它们。
  • 如果你阻止它接受某个状态的 touch 事件时,可能导致结果不确定。不过,我们在实际中应该不会这样做。

需求

事件传递的最终目标是找到一个能够处理响应这个事件的对象(UIResponder 的子类)。如果找不到就丢弃它。

前提条件

能够处理事件的对象需要完成下面3个条件:

  • 实现这四个方法
1
2
3
4
5
6
7
8
9
10
11
12
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
* 可以交互的。即 userInteractionEnabled 属性为 YES
* 是可见的。即 hidden = NO & alpha > 0.01

UIKit 已有的轮子

换汤不要药,跟前面的前提条件一样,只不过是另外一种形式来完成而已。

gesture recognizer

通过实现跟 UIResponder 相同签名的方法来完成。参考例子,上面有提到,官方文档 Listing 1-8 Implementation of a checkmark gesture recognizerYYGestureRecognizer

1
2
3
4
5
6
7
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

UIControl

也是内部实现跟 UIResponder 相同功能的方法来完成,里面通过一个 _targetActions 数组来存储各种 UIControlEvents 状态的事件。可以参考Chameleon UIControlSVSegmentedControl

1
2
3
4
5
6
7
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)cancelTrackingWithEvent:(UIEvent *)event;

过程

手指触摸屏幕就会生成 UIEvent 对象,然后放在 application 的队列里面,application 会从系统队列的顶层取出一个事件并分发它。application(sendEvent:) -> window(sendEvent:) -> initial object(hit-test view or frist responder)。
而 application 和 window 则是通过 Hit-Testing 和响应链来找到 initial object。一般情况下,都不需要我们去干涉 UIKit 的这个分发过程。但是,我们可以在这个过程去干涉达到自己的需求。

用途

这个章节的相关代码参考自

扩大触摸区域

我们绘制 UIButton 的时候,想要扩大它的响应区域。我们可以在 UIButton 里面处理 Hit-Testing 的那两个方法其中一个里面做处理。

hitTest:withEvent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGRect touchRect = CGRectInset(self.bounds, -10, -10);
if (CGRectContainsPoint(touchRect, point)) {
return self;
}
return [super hitTest:point withEvent:event];
}

pointInside:withEvent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
}
CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
CGRect hitTestingBounds = bounds;
if (minimumHitTestWidth > bounds.size.width) {
hitTestingBounds.size.width = minimumHitTestWidth;
hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2;
}
if (minimumHitTestHeight > bounds.size.height) {
hitTestingBounds.size.height = minimumHitTestHeight;
hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2;
}
return hitTestingBounds;
}

superview 响应 subview 的事件

这个在限制事件分发到 subviews 上小节里面就有说过。重载 hitTest:withEvent: 方法返回自己 self。

iOS 事件处理(1)

From: http://joakimliu.github.io/2017/01/15/iOS%E7%9A%84%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86/

最近把 Event Handling Guide for iOS 看了几遍,算是对 iOS 的事件处理有了个整体的概念,本文较长,可以先看后面的总结部分。对于事件处理,我们最熟悉的莫过于下面的Target-Action模式代码。

1
2
3
4
5
6
7
UIControl
addTarget:action:forControlEvents:
UIGestureRecognizer
initWithTarget:action:

这些都是比较高级的用法了,因为 UIKit 都帮我们处理了,Gesture recognizers convert low-level event handling code into higher-level actions.低级的事件处理就是所谓的自定义事件处理。在这之前,我们先谈谈 iOS 中表示事件的相关类。

UIEvent

A UIEvent object (or, simply, an event object) represents an event in iOS. 在 iOS 中,事件是由UIEvent类表示的,大致可以分为四种类型

1
2
3
4
5
6
7
8
9
10
11
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches, // 触摸事件,按钮、手势等
UIEventTypeMotion, // 运动事件,摇一摇、指南针等
UIEventTypeRemoteControl, // 远程控制,耳机等
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0), // 3D touch
};

这里只说 UIEventTypeTouches 触摸事件(注:本文所说的事件都是触摸事件)。 event 里面包含一个或者多个 touch (代表手指触摸屏幕,由 UITouch 类表示,下面会说)。当触摸事件发生时,系统会将它路由到合适的响应者,然后通过 UIResponder 的 touchesBegan:withEvent: 等方法传递。系统会评估这个事件并且找到合适的对象来处理它(包括hit-testingfirst responder),一般情况下,我们不需要做特殊的处理。所以 UIEvent 类里面有多个获取 UITouch 对象的方法:

1
2
3
4
5
6
7
8
9
10
11
- (nullable NSSet <UITouch *> *)allTouches;
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_AVAILABLE_IOS(3_2);
- (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);
- (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);

这里有两点需要注意一下:

  • timestamp 属性,这个时间戳是从系统开机后开始算的。相关事件处理,可以看Peak君的iOS关于时间的处理
  • You should never retain an event object or any object returned from an event object. 不要持有事件对象或者从事件 对象中返回的对象。因为 UIEvent 对象在多点触摸序列(指手指触摸屏幕到离开屏幕)中是持久的, UIKit 会重用 UIEvent 对象,如果你需要持有 event 或者 touch 的相关信息时,你可以拷贝相关信息,赋值给相关变量。

UITouch

它代表手指触摸到屏幕上的位置,大小等相关信息。一个手指代表一个 UITouch 对象,所以可以根据tapCount属性来判断是单击、双击、三击。根据 touch 可以我们可以得知

  • The view or window in which the touch occurred (touch 发生所在的 view 或者 window)
  • The location of the touch within the view or window (touch 发生所在的 view 或者 window 上的位置)
  • The approximate radius of the touch
  • The force of the touch (on devices that support 3D Touch or Apple Pencil)

当然还可以知道接受该 touch 对象的 gestureRecognizers 手势识别器。

我们还可以根据 phase 属性获取 touch 的相关状态。

1
2
3
4
5
6
7
8
9
10
UITouchPhaseBegan: A finger for a given event touched the screen.
UITouchPhaseMoved: A finger for a given event moved on the screen.
UITouchPhaseStationary: A finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded: A finger for a given event was lifted from the screen.
UITouchPhaseCancelled: The system canceled tracking for the touch, as when (for example) the user moves the device against his or her face.

除了 UITouchPhaseStationary 状态以外,每个状态都对应着 UIResponder 的touchXxxxx:withEvent:类似的方法。拿到 UITouch ,我们就可以指定这个触摸事件所在的 view 以及位置。拿到位置以后,我们就可以做我们想要做的事情了。

UIResponder

The UIResponder class defines an interface for objects that respond to and handle events. It is the superclass of UIApplication, UIView and its subclasses (which include UIWindow). Instances of these classes are sometimes referred to as responder objects or, simply, responders. 它定义了响应和处理事件的接口。像一些能够处理相应事件的类(UIApplication, UIView 等)都是它的子类。

1
2
3
4
5
6
7
8
9
// 处理触摸事件的主要方法,所以我们自定义事件处理时,就需要在这几个方法里面做文章。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; // 一根或者多跟手指开始触摸屏幕
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; // 手指在屏幕上移动
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; // 手指离开屏幕
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; // 收到系统的事件(如,来电话了或者低内存警告等)取消触摸事件

只要手指触摸在屏幕上,不管是手指拖动还是离开屏幕, UIEvent 对象就会生成,而它由包含相关的 UITouch 对象。入参说明:

  • touches。这个状态新的或改变的 touches 。
  • event。代表这个事件 event 的所有 touches ,所以上面的 touches 也属于它。This differs from the set of touches because some of the touch objects in the event may not have changed since the previous event message. 它跟上面的 touches 的区别就在于它可能包含发生改变的 touch 。强调一个状态的改变。

管理响应链的方法

nextResponder

Returns the receiver'€™s next responder, or nil if it has none. 接受者的下一个响应者,如果没有的话就为 nil。nextResponder 就是响应链中下一个处理事件的对象。

UIResponder 类不会自动存储 nextResponder,所以默认返回 nil。子类化的时候需要重载该方法自己设置。一般情况下,UIView 一般返回它所在的 UIViewController 或者它的 superview;而 UIViewController 返回它 view 的 superview 或者 UIViewController (会一直循环找,直到找到 UIWindow -> UIApplication);UIWindow 返回 UIApplication;UIApplication 返回 nil。所以,响应链在视图层级构建的时候就已经形成了。

isFirstResponder

Returns a Boolean value indicating whether the receiver is the first responder. 是否是第一响应者。默认为 YES。

canBecomeFirstResponder

Returns a Boolean value indicating whether the receiver can become first responder. 是否能够成为第一响应者。默认为 NO。

如果返回 YES ,就说明它能够成为第一响应者,it becomes the first responder and can receive touch events and action messages.能够接受触摸事件和动作消息。子类如果想要成为第一响应者,那么必须重载这个方法。注意,你只有当 view 已经添加到 view 层级里面才能发送这个消息(becomeFirstResponder),不然这个结果是不确定的,例子如下:

Note: Make sure that your app has established its object graph before assigning an object to be the first responder. For example, you typically call the becomeFirstResponder method in an override of the viewDidAppear: method. If you try to assign the first responder in viewWillAppear:, your object graph is not yet established, so the becomeFirstResponder method returns NO.
注意:在一个对象成为 first repsonder 之前要确保建立好 object graph。例如,你通常在 viewDidAppear: 方法里面调用 becomeFirstResponder。如果 viewWillAppear: 方法里面设置 first responder,这个时候 object graph 还没建立好,所以 becomeFirstResponder 会返回 NO。

becomeFirstResponder

Notifies the receiver that it is about to become first responder in its window. 报告接受者它将要在 window 上成为为第一响应者。默认返回 YES。

A responder object only becomes the first responder if the current responder can resign first-responder status (canResignFirstResponder) and the new responder can become first responder. 只有当前的响应者能够辞去第一响应者,新的响应者才能够成为第一响应者。也就是说第一响应者永远只有一个。

If the view’s window property holds a UIWindow object, it has been installed in a view hierarchy;if it returns nil, the view is detached from any hierarchy. view 的 window 属性持有 UIWindow 对象时才表示这个 view 已经添加到 view 层级中。也就说只有 view 层级确定成功后才能成为第一响应者。

canResignFirstResponder

Returns a Boolean value indicating whether the receiver is willing to relinquish first-responder status. 是否能够将要放弃作为第一响应者的状态。默认为 YES。

As an example, a text field in the middle of editing might want to implement this method to return NO to keep itself active during editing. 例如,编辑中的文本输入框可能想实现这个方法返回 NO 来保持自己的编辑状态(不过,这种情况目前还没有遇到过。)。

resignFirstResponder

Notifies the receiver that it has been asked to relinquish its status as first responder in its window.通知接受者它被询问是否放弃在 window 上作为第一响应者的状态。 默认为 YES。 注意:子类重载该方法的时候,必须实现父类的方法。

简单实例

点击某个 view 出现 copy 等菜单的 UIMenuController 时,我们会重载 canBecomeFirstResponder 方法并返回 YES;

A major role of your app’s application object is to handle the initial routing of incoming user events. It dispatches action messages forwarded to it by control objects (instances of the UIControl class) to appropriate target objects. application 主要的职责是处理用户事件。

sendEvent:
分发一个消息给合适的响应者对象。你可以子类 UIApplication 对象并且重载这个方法来拦截事件。但是拦截完后记得调用父类的实现 [super sendEvent:event]。

sendAction:to:from:forEvent:
转发消息给特定的对象。
target:接受消息的对象,如果为 nil,那么 APP 会发送给第一响应者,然后沿着响应链传递。
sender:发送 action 消息的对象。默认的 sender 是 UIControl 对象。

UIControl

The UIControl class implements common behavior for visual elements that convey a specific action or intention in response to user interactions. UIControl 为响应用户的交互而对那些可见的元素实现了共同的行为,其实也是事件的高级处理。它使用来 Target-Action 机制向 APP 报告用户的交互。

UIControl 由 UIControlState 类型的属性 state 决定它的外观和支持用户交互的能力。

The control handles all the work of tracking incoming touch events and determining when to call your methods.处理所有的跟踪将要来的触摸事件的工作,并且决定什么时候调用你的方法。通过 addTarget:action:forControlEvents: 方法添加 target 和 action ,target 可以为任何对象,一般是包含 control 的 view controller,如果 target 为 nil,那么控件会通过响应链查找定义了该方法的响应者。

The control maintains a list of its attached targets and actions along and the events each supports.里面维持了一个数组来存储它的 target、action 已经所支持的事件。control 不会 retain target。可以参考iOS-Runtime-Headers _targetActions数组

用 Xcode 在 UIControl 的响应事件里面断点可以看到 _targetActions 数组
Xcode lldb for _targetActions

sendActionsForControlEvents:

This method iterates over the control’s registered targets and action methods and calls the sendAction:to:forEvent: method for each one that is associated with an event in the controlEvents parameter.iterates 可以看出 UIControl 里面是维持了一个数组。

响应方法有三种形式

1
2
3
4
5
- (IBAction)doSomething;
- (IBAction)doSomething:(id)sender;
- (IBAction)doSomething:(id)sender forEvent:(UIEvent*)event; // sender就是调用这个方法的对象,一般就是control自己;而event就是触发这个control的相关事件

根据 UIControlEvents 来指定用户交互的特定形式,例如:UIButton 就是 UIControlEventTouchDown 或者 UIControlEventTouchUpInside 触发 action 方法,而 UISlider,则是 UIControlEventValueChanged。
When a control-specific event occurs, the control calls any associated action methods right away. Action methods are dispatched through the current UIApplication object, which finds an appropriate object to handle the message, following the responder chain if needed. For more information about responders and the responder chain, see Event Handling Guide for iOS. 当一个特定的事件发生时,control 就正确的调用相关的 action 方法。通过 UIApplication 对象(它能够找到相应的对象来处理消息)来分发 action 方法,如果需要的话,则通过响应链来找到。

子类化 UIControl 使你能够简单支持事件处理。用下面两种方法中的一种来改变它的行为。

  • 重载 sendAction:to:forEvent: 方法,观察或者修改分发 action 方法到相关的 target;
  • 重载 beginTrackingWithTouch:withEvent:, continueTrackingWithTouch:withEvent:, endTrackingWithTouch:withEvent:, cancelTrackingWithEvent: 方法当事件发生时,跟踪它们。用这些方法代替 UIResponder 定义的 touchXxx:withEvent: 方法;

sendAction:to:forEvent:
调用一个特定的方法。这个方法带着提供的信息并且将它转发给单例 UIApplication 去分发。

beginTrackingWithTouch:withEvent:

当 touch 事件发生在控件里面时会调用这个方法。

Target-action is a design pattern in which an object holds the information necessary to send a message to another object when an event occurs. Target-action
是一种设计模式,当某个事件发生时,持有信息的对象发送消息给另外一个对象。持有的信息包括接受消息的对象以及消息。

Apple Target-Action

上面图片所表示的可以用下面的代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// viewcontroller
- (void)viewDidLoad {
[super viewDidLoad];
UIControl *control = [[UIControl alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[control addTarget:self action:@selector(restoreDefaults:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:control];
}
- (void)restoreDefaults:(id)sender {
}

即 UIControlEventTouchUpInside 类型的事件发生时,事件会传递到 control 对象,然后由 control 去触发 target 的 action 行为。UIGestureRecognizer 也是类似的。

上面有提到 Gesture recognizers convert low-level event handling code into higher-level actions.

UIGestureRecognizer is an abstract base class for concrete gesture-recognizer classes.

A gesture recognizer doesn’t participate in the view’s responder chain. 尽管它是添加在 view 上的,但是它不参与 view 的响应链。

那来看看 Gesture Recognizers 是怎么个高级法? 当它添加到 view 上时,它能够让 view 像 control 一样响应特定的事件。

![Apple A gesture recognizer attached to a view][9]

内置手势以及其用法

系统已经帮我们内置几个非常实用的手势:

  • UITapGestureRecognizer:Tapping (any number of taps)。点击手势,例如:单击、双击、三击。
  • UIPinchGestureRecognizer:Pinching in and out (for zooming a view)。缩放手势,例如:相册放大缩小。
  • UIPanGestureRecognizer:Panning or dragging。拖拽手势,例如:scrollview 的拖动滚动。
  • UISwipeGestureRecognizer:Swiping (in any direction)。滑动手势,例如:浏览相册。
  • UIRotationGestureRecognizer:Rotating (fingers moving in opposite directions)。旋转手势,例如:两个手指旋转相册的照片。
  • UILongPressGestureRecognizer:Long press (also known as “touch and hold”)。长按手势,例如:朋友圈长按文本出现复制等菜单。
  • UIScreenEdgePanGestureRecognizer: swipe up from the bottom of the screen to reveal Control Center。从屏幕下面从下往上滑动,出现控制中心。

每个 gesture recognizer 都跟一个 view 相关联,所以它得添加到 view 上。一个 view 可以有多个 gesture recognizer,通过gestureRecognizers属性来获取。

When a user touches that view, the gesture recognizer receives a message that a touch occurred before the view object does. As a result, the gesture recognizer can respond to touches on behalf of the view. 当用户触摸 view 的时候,gesture recognizer 会在 view(靠 touchBegan、moved、ended、cancelled:withEvent:这几个方法来处理 touch 事件)之前收到这个 touch 事件。所以 gesture recognizer 能够代表 view 来响应这个 touch。

gesture recognizer 分为离散的和连续的。从下图可以看出,离散的只会发送一次action message给 target ,而连续的则会发送多次直到这个触摸队列完成。

![Apple Discrete and continuous gestures][10]

  • 离散的有:UITapGestureRecognizer、UISwipeGestureRecognizer;
  • 连续的有:UIPinchGestureRecognizer、UIPanGestureRecognizer、UIRotationGestureRecognizer、UILongPressGestureRecognizer;

使用手势的方法也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (void)viewDidLoad {
[super viewDidLoad];
// Create and initialize a tap gesture
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(respondToTapGesture:)];
// Specify that the gesture must be a single tap
tapRecognizer.numberOfTapsRequired = 1;
// Add the tap gesture recognizer to the view
[self.view addGestureRecognizer:tapRecognizer];
}
// Respond to a rotation gesture 离散的手势需要在响应方法里面判断它的状态
- (IBAction)showGestureForRotationRecognizer:(UIRotationGestureRecognizer *)recognizer {
if (([recognizer state] == UIGestureRecognizerStateEnded) || ([recognizer state] == UIGestureRecognizerStateCancelled)) {
[UIView animateWithDuration:0.5 animations:^{
self.imageView.alpha = 0.0;
self.imageView.transform = CGAffineTransformIdentity;
}];
}
}

了解 gesture recognizer 的交互

gesture recognizer 的状态

gesture recognizer 可以从一个状态变换到另外一个状态。

![Apple State machines for gesture recognizers][11]

根据某种特定的条件,状态是会变的。离散手势和连续手势的机制不同。
离散手势直接从 Possible -> Failed or Recognized。注意,Ended 是 Recognized 的别名,其实这两个状态都代表手势结束了。

只要 gesture recognizer 改变它的状态,它就会给 target 发送 action message ,除非它的状态变为 Failed 或者 Canceled 。所以,离散的手势就会当状态从 Possible -> Recognized 的时候发送一次 action message 。而 连续手势会发送多次。当 gesture recognizer 到达 Recognized 状态的时候,它会 reset 重置到 Possible 状态(到这个状态不会发送 action message )。(在后面提到的,自定义手势的时候,置为 Recognized 的时候也要手动 reset 重置,将该 gesture recognizer 的一些属性啥的都置为初始状态。)注意:变为 Failed 或者 Canceled 是不会发送消息的。

与其他手势的交互

一个 view 可以有多个 gesture recognizer 。通过 gestureRecognizers 属性可以知道该它有多少个 gesture recognizer 。当然也可以通过 addGestureRecognizer: 和 removeGestureRecognizer: 方法添加、移除某个 gesture recognizer。那么问题来了,如果有多个 gesture recognizers 的话,怎么处理这些事件,它们之间会存在着竞争关系。

When a view has multiple gesture recognizers attached to it, you may want to alter how the competing gesture recognizers receive and analyze touch events. By default, there is no set order for which gesture recognizers receive a touch first, and for this reason touches can be passed to gesture recognizers in a different order each time. 如果一个 view 上有多个 gesture recognizer 时,你可能想改变这些竞争手势如何接受和处理触摸事件的。默认情况下,这些手势谁第一个接受到 touch 是无序的,所以手势可能每次都发生在不同的顺序。所以,我们可以用 delegate 和子类化来处理改变这些行为。

  • Specify that one gesture recognizer should analyze a touch before another gesture recognizer. (指定某个手势发生在另外一个手势前面)
  • Allow two gesture recognizers to operate simultaneously(允许两个手势同时发生).
  • Prevent a gesture recognizer from analyzing a touch(防止某个手势发生).

指定两个手势的触发顺序

通过下面的相关代理指定某个手势识别的时候另外一个手势识别失败。

  • requireGestureRecognizerToFail: 方法 iOS7之前的处理方式
  • gestureRecognizer:shouldRequireFailureOfGestureRecognizer: 代理方法
  • gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer: 代理方法

防止某个分析触摸事件

手势的处理使用是analyze分析到handle处理。这个比上面更吊,根本就不让分析触摸事件。通过下面两个代理方法来完成。

  • gestureRecognizer:shouldReceiveTouch: (触发的时候才会走这个代理,有一个新 touch 也会走。默认为 YES。如果为 NO,当一个 touch 发生的时候,该手势就不会被通知。这个方法不会改变手势的状态。)
  • gestureRecognizerShouldBegin: (走出 Possible 状态后才能知道,所以它的发生时间比上个方法要晚。如果 UIView 或者 UIControl 的子类需要处理自定义的事件处理时,而需要与某个手势竞争时。该方法返回 NO ,使手势的状态马上变为 fail,使其他触摸处理事件去执行。)

允许同时发生

是两个手势同时发生。按道理两个手势是不能同时响应的,但是有的时候你希望 pinch 和 rotate 手势同时发生,可以使用下面的代理方法。

1

gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:

Note:You need to implement a delegate and return YES on only one of your gesture recognizers to allow simultaneous recognition. However, that also means that returning NO doesn’t necessarily prevent simultaneous recognition because the other gesture recognizer's delegate could return YES. 注意:实现一个代理,让它返回 YES,就能够允许同时发生。当然它也意味着返回 NO 不能防止它不同时发生,因为很有可能其他的代理返回 YES。

指定两个手势间的单程关系

子类重载canPreventGestureRecognizer:或者canBePreventedByGestureRecognizer:方法返回 NO 来处理。默认返回 YES。
例如,rotation 能够防止 pinch,而 pinch 不能防止 rotation,就可以用下面的代理来处理。

[rotationGestureRecognizer canPreventGestureRecognizer:pinchGestureRecognizer];

或者重载 rotation 手势的方法返回 NO。

与其他控件交互

In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior.iOS6 以后,默认的 control actions 控制会阻止覆盖手势行为。例如,按钮的事件就是一个 tap 手势。如果你在按钮的父视图上添加了一个手势,那么当用户点击按钮的时候,按钮会收到响应触摸事件,它的父视图不会响应。当然这仅仅作用于默认有control action的手势识别上,它还包括:

  • tap 手势在 UIButton,UISwitch,UIStepper,UISegmentControl,UIPageControl;
  • swipe 手势在 UISlider;
  • pan 手势在 UISwitch;

当你想要重载 control 默认的 action,在 control 上面添加手势时,手势第一次收到触摸事件。但是你得注意了,需要去看看iOS Human Interface Guidelines以确保能够为用户提供直觉的体验(这样做是不推荐的,记得看view programming的时候看到不推荐在 UIButton 上面手势等之类的事件)。

手势处理原生的触摸事件

那么手势是怎样处理一个 view 的触摸事件呢?我们先了解 touches 和 events 的术语。

event 包括所有在当前触摸队列里面的 touches

UIEvent 和 UITouch 在前面已经讲了。这里的Multitouch Sequence触摸队列是指从手指触摸屏幕开始到最后手指离开屏幕结束。还有需要注意的是,手指的精确性比鼠标要低。并且它的触摸区域是椭圆形的,比用户期望的要差。它还受手指的大小、方向、压力、哪根手指以及其他因素的影响。

APP 接受触摸事件都是在 Touch-Handling Methods

前面讲 UIResponder 的时候,已经提到,在触摸队列中,当某个 touch phase 有新的或改变的 touch 发生时,APP 就会通过下面的方法发送消息:

1
2
3
4
5
6
7
8
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

每个方法都对应一个 touch phase。例如,touchesBegan:withEvent: 对应着 UITouchPhaseBegan。注意:这些方法跟 gesture recognizer 状态(例如:UIGestureRecognizerStateBegan)没有关系。

控制 touch 传递到 view

你有多少次想 view 在 gesture recognizer 之前收到 touch 。
view 的 touch 分发是从 UIApplication -> UIWindow 。然后 window 在发送 touch 到 view 之前,会先发给 view (或者它 superView 上)的 gesture recognizers。

![Apple Default delivery path for touch events][12]

手势首先识别 touch

A window delays the delivery of touch objects to the view so that the gesture recognizer can analyze the touch first. window 会延迟传递 touch 对象到 view,所以手势能够最先识别这个 touch。如果手势识别了这个触摸事件,那么 window 不会分发剩下到 view 上,并且还会取消之前发送出去的触摸事件。

例如,有一个离散的手势需要两根手指,所以有两个 touch 对象,传递流程如下图所示。

![Apple Sequence of messages for touches][13]

  • The window sends two touch objects in the Began phase—through the touchesBegan:withEvent: method—to the gesture recognizer. The gesture recognizer doesn’t recognize the gesture yet, so its state is Possible. The window sends these same touches to the view that the gesture recognizer is attached to.
  • The window sends two touch objects in the Moved phase—through the touchesMoved:withEvent: method—to the gesture recognizer. The recognizer still doesn’t detect the gesture, and is still in state Possible. The window then sends these touches to the attached view.
  • The window sends one touch object in the Ended phase—through the touchesEnded:withEvent: method—to the gesture recognizer. This touch object doesn’t yield enough information for the gesture, but the window withholds the object from the attached view.
  • The window sends the other touch object in the Ended phase. The gesture recognizer now recognizes its gesture, so it sets its state to Recognized. Just before the first action message is sent, the view calls the touchesCancelled:withEvent: method to invalidate the touch objects previously sent in the Began and Moved phases. The touches in the Ended phase are canceled.

假如最后一步,gesture recognizer 还没有识别到,那么它的状态就变成 failed,然后 window 就会将那两个 touch 对象通过 touchesEnded:withEvent: 消息传递给它所附属的 view。

连续的 gesture recognizer 跟上面的行为差不多,除了它可能在 Ended phase 之前就已经识别到那个手势了。一旦识别到手势,它就会变成 Began 状态。window 就会把触摸队列中剩下的 touch 对象都发送给这个 gesture recognizer,而不是它所附属的 view。

响应 touch 传递到 view

所以我们可以改变一些 gesture recognizer 的属性来改变默认的事件分发路径。可以参考上面离散两个手指的例子来理解下面的属性。

  • cancelsTouchesInView:默认为 YES。当 gesture recognizer 识别到手势后,window 不会分发它们给 view。并且 window 会通过 touchesCancelled:withEvent: 方法来取消之前传递的 touch。如果 gesture recognizer 没有识别到,那么 view 会收到触摸队列里面所有的 touch。

  • delaysTouchesBegan: 默认为 NO。正常情况下,window 发送 began 和 moved 状态的 touch 给 view 和 gesture recognizer。将它置为 YES,防止 window 发送 Began phase 给这个 view。这能够保证当 gesture recognizer 识别到 touch 时,没有任何 touch 事件分发到这个它附属的 view 上。小心设置这个属性,因为它会使你界面感觉没有响应。这个属性跟 UIScrollView 的 delaysContentTouches 属性类似。当手指触摸滚动开始后,scrollview 的所有 subview 不会接受 touch 事件,所以没有闪光的视觉反馈。

  • delaysTouchesEnded:默认为 YES。当它为 YES 时,它能保证 view 不会马上完成一个动作,因为 gesture recognizer 后面可能会想要取消。当 gesture recognizer 正在分析一个 touch 事件时,window 不会分发 Ended phase 状态的 touch 给所它附属的view。如果 gesture recognizer 识别到了,那么 touch 将会被取消(即不会分发给 view)。如果 gesture recognizer 识别不到,那么 window 会通过 touchesEnded:withEvent: 消息分发这些 touch 对象给 view。设置它为 NO 时,允许 view 和 gesture recognizer 同时在 Ended phase 分析这些 touch 对象。 例如,当一个 view 有一个双击手势时。delaysTouchesEnded 为 YES,这个 view 收到 touchesBegan:withEvent:, touchesBegan:withEvent:, touchesCancelled:withEvent:, and touchesCancelled:withEvent: 的消息。当它为 NO 时,它会收到 touchesBegan:withEvent:, touchesEnded:withEvent:, touchesBegan:withEvent:, and touchesCancelled:withEvent:,这就意味着在 touchesBegan:withEvent: 时,就能识别它是 double tap(实际情况,应该是在 End 才识别到,Apple 自定义模拟处理单击手势时就是在 touchesEnded:withEvent: 方法里面处理的)。

如果 gesture recognizer 检测到一个 touch 不属于它,它能够直接传递给它的 view。通过 gesture recognizer 调用 ignoreTouch:forEvent: 方法,将这个 touch 传递。(问题:难道如果 gesture recognizer 处理不了,window 就不会将它传递给它所附加的 view 吗?view 是能够接受到 touch 的,gesture recognizer 只是一个高级的封装,所以 window 会传下去。)

自定义手势

自定义手势需要创建 UIGestureRecognizer 的子类。需要引入

1

#import

通过实现下面的方法。

1
2
3
4
5
6
7
8
9
10
- (void)reset;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

它的签名和 UIResponder 的那个四个方法签名一样。你重载这些方法的时候,必须调用父类的实现,即使它是个空的实现。重要的是在这些方法里面改变 status 属性的值,并且在 reset 方法里面将一些值置为初始值(因为 gesture recognizer 转成 Recognized/Ended, Canceled, or Failed 时,reset 方法在变成 Possible 状态前会被调用)。

官方文档 Listing 1-8 Implementation of a checkmark gesture recognizer 有实现一个自定义的手势,这里就不贴代码了。还可以看看YYGestureRecognizer

我们想动态的响应事件。例如:一个 touch 可以出现在屏幕上的不同对象上,你必须去决定你想要哪个对象去响应这个事件,理解这些对象怎样接受这个事件。

当用户的触摸事件发生时,UIKit 创建一个事件对象,它包含了需要处理这个事件的一些信息。然后它将这个事件对象放在 App 的 event queue 里面(这里就跟 runloop 有关系了)。对于 touch 触摸事件,这个事件对象就是在 UIEvent 对象。对于 motion 事件,这个事件对象就取决于你使用的是哪个 framework 和哪种你感兴趣的 motion 事件了。

一个事件会以一条特定的路线去传递,直到它找到可以处理它某个对象为止。首先,UIApplication 从系统队列的顶层取出一个事件并分发它。通常,这个事件会被发送给 key window,key window 会将它发送给 initial object 去处理。initial object 取决于事件类型。

  • Touch events. window 第一次尝试将事件发送给 touch 发生的那个 view 。这个 view 就是所谓的 hit-test view。找 hit-test view 的过程被称为 hit-testing。(它的寻找顺序是从下至上,而响应链则是从上至下,下面会提到,最下面的是 application,最上面的就是 initial object)
  • Motion and remote control events. window 发送 shaking-motioin 或者 control event 给 frist responder 去处理。即第一响应者。

这些事件路径的最终目标就是找到一个能够处理响应这个事件的对象。所以,UIKit 第一次会将它发送给最适合来处理它的对象。就是上面提到的 hit test view 或者 first responder。

Hit-Testing 返回 touch 发生的那个 view

iOS 用 hit-testing 去寻找 touch 下面的那个 view。hit-testing 会检测 touch 是否在相关 view 的 bounds (这里是 bounds,而不是 frame)里面。如果是,会循环检查这个 view 的所有 subviews。view 层级最低(也就是是最上面那个 subview)的包含这个 touch point 的就是 hit-test view。然后 iOS 会将这个 touch 事件交给这个 view 去处理。这里有张 hit-testing 的经典图。它会从 window 开始从下往上开始寻找。

Apple Hit-testing returns the subview that was touched

hitTest:withEvent: 方法根据给定的入参 CGPoint 和 UIEvent 返回 hit test view。在调用 hitTest:withEvent: 方法之前会先调用 pointInside:withEvent: 方法。如果传到 hitTest:withEvent: 的 point 在这个 view 的 bounds 里面,那么 pointInside:withEvent: 返回 YES。然后,会循环的在每个返回 YES 的 subview 上调用 hitTest:withEvent: 方法。

如果传进去的 point 不在 view 的 bounds里面,那么第一次调用 pointInside:withEvent: 会返回 NO,这个 point 会被忽略掉,hitTest:withEvent: 返回 nil。如果某个 subview 返回 NO,那么它这个 view 的整个层级都是被忽略掉的,因为既然它都不会出现在 subview 上,那么自然不会出现在 subview 的 subview 上面嘛。这就意味在一个 subview 上的任何点,它如果在 superview 之外,是接受不到触摸事件的。当 subview clipsToBounds 属性为 NO (允许 subview 超过 superview 的边界)时,这个事情会发生。

Note: A touch object is associated with its hit-test view for its lifetime, even if the touch later moves outside the view.
注意,一个 touch 对象在 hit-test view 的生命周期内都跟它关联的,即使这个 touch 后面移动到它外面。

hit-test view 给了第一次去处理这个触摸事件的机会。如果 hit-test view 处理不了,那么它会沿着响应链向 application 的方向去寻找可以处理它的对象。

响应链由响应对象组成

许多类型的事件都依赖响应链去分发。响应链是由一系列相连接的响应对象组成(在 view 的层级确定后,响应链就连接完成了)。它由 first responder 开始,application object 结束(它的传递顺序是从上至下的)。如果 frist responder 不能处理这个事件,那么会在响应链里将这个事件向前转发。一个响应对象它能够响应处理事件,UIResponder 就是它的基类(上面有提到过)。像 UIApplication, UIViewController, 和 UIView 都是 responders,但是注意,Core Animation layers 不是。

first responder 指定首先收到事件。通常,first responder 是一个 view。关于 UIResponder,可以看上面的相应内容。

Events不是依赖响应链的唯一对象。响应链还用于以下:

  • Touch events. 如果 hit-test view 不能处理 touch event,那么会由 hit-test view 开始走响应链。
  • Motion events.
  • Remote control events.
  • Action messages. 当用户操作一个 control,例如 button 或者 switch,并且 target 的 action method 为 nil,message 通过响应链从 frist responder 开始传递,也可以是这个 control view 自己。
  • Editing-menu messages. cut: copy: paste: 等等。 Displaying and Managing the Edit Menu
  • Text editing. 自定义输入内容,应该有自定义键盘的例子。 Custom Views for Data Input

响应链的分发路径

如果 initial object(hit-test view 或者 the first responder,它通常是个 view)处理不了这个事件,UIKit 会在响应链中将它传递给 next responder。每个响应对象决定它是否处理这个事件,还是通过调用nextResponder传递下去。
这个过程持续到 app object,如果 app object 都处理不了,那么就丢弃掉这个事件。

Apple The responder chain on iOS

Important: If you implement a custom view to handle remote control events, action messages, shake-motion events with UIKit, or editing-menu messages, don’t forward the event or message to nextResponder directly to send it up the responder chain. Instead, invoke the superclass implementation of the current event handling method and let UIKit handle the traversal of the responder chain for you.

重要:不要直接调用 nextResponder 方法,而应该调用父类当前事件处理的实现,让 UIKit 来处理。

通常情况下,我们能够用 UIKit 里面标准的 control 和 gesture recognizers 来处理几乎所有的触摸事件了。当然有些情况,我们需要自定义,也就是上面所提及的高层和低级的问题。

创建 UIResponder 的子类

首先就是要创建 UIResponder 的子类,也可以是 UIView、UIViewController、UIControl、UIApplication、UIWindow 的子类,不过子类化 UIApplication、UIWindow 比较罕见,一般都是继承 UIView、UIControl。

然后还有3件事需要处理

  • 实现 touchXxx:withEvent: 等 Touch-Event Handling 相关方法;
  • userInteractionEnabled 置为 YES;
  • 它是可见的,不能隐藏或者透明。即 hidden = NO & alpha > 0.01。

实现 Touch-Event Handling 方法

在触摸队列中,App 发送一系列的事件消息给目标响应者。为了接受处理这些消息,这个响应者对象必须实现下面的 UIResponder 的事件处理方法

1
2
3
4
5
6
7
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

每个方法都对应一个 touch 对象的状态,UITouchPhaseBegan,UITouchPhaseMoved,UITouchPhaseEnded,UITouchPhaseCancelled。当有某个状态有新的或者改变的 touch 对象时,App 就会调用上面的相关方法。

入参说明:

  • touches。这个状态新的或改变的 touches。
  • event。代表这个事件 event 的所有 touches,所以上面的 touches 也属于它。它跟上面的 touches 的区别就在于它可能包含没有发生改变的 touch。强调一个状态的改变。

处理触摸事件的所有 view 都希望能够收到完整的事件流,所以在创建你的子类时,要注意

  • If your custom responder is a subclass of UIView or UIViewController, you should implement all of the event handling methods.(view 或者 viewcontroler 要实现所有的事件处理方法)
  • If you subclass any other responder class, you can have a null implementation for some of the event methods.(其他 responder 类的子类,可以不实现其中某个事件处理方法)
  • In all methods, be sure to call the superclass implementation of the method.(确保调用父类的实现方法)

当然如果事件的某个状态没有接受处理,这会导致后果可能不定义的或者不良的。如果在事件处理的时候创建了恒久的对象,那么在 touchesCancelled:withEvent: 方法里,记得销毁这些对象,即让它们回到原始状态。当有电话打进来时,app 就会调用 touchesCancelled:withEvent: 方法。然后在 touchesEnded:withEvent: 方法里面也要销毁这些东西,回到原始状态,强调一个有始有终。

iOS 推送证书更新

今天提示我应用的推送证书要过期了,然后更新了下,做个笔记.

1 (可选)删除mac 上老的证书密钥啥的

2 和原本一样 mac 钥匙串请求证书(注意名称取个详细点的好区分)(留着备用)

3 进入开发者中心 找到自己的app id 展开后点击下面的编辑

4 编辑push 部分,开发者证书创建 发布证书先revoke 然后在创建 (因为正式的有效期多一个月)

5 选择第二部请求的证书

6 上步创建后这里 下载在本机点击安装 然后完成 (开发和正式基本一样最后两个证书安装)

7 安装后 在钥匙串中找到导出p12(注意展开是有私钥的)

8 上传p12 文件 (注 部分平台可以直接使用, php记得有博客需要一个制作流程,)

到此push 证书更新完毕

iOS 9 通用链接(Universal Links)

From: http://strivingboy.github.io/blog/2015/09/27/ios9/

“What is Universal Links?”

Apple 推出通用链接:一种能够方便的通过传统 HTTP 链接来启动 APP, 使用相同的网址打开网站和 APP。

通过唯一的网址, 不需要特别的schema就可以链接一个特定的视图到APP 里面 。比如:在微信中使用了通用链接, 那么用户在Safari、UIWebView或者 WKWebView点击一个链接, iOS设备上的微信app怎会在微信里面自动打开这个页面, 如果没有安装则在Safrai中打开响应链接。

NOTE: Universal links let iOS 9 users open your app when they tap links to your website within WKWebView and UIWebView views and Safari pages, in addition to links that result in a call to openURL:, such as those that occur in Mail, Messages, and other apps.

For users who are running versions of iOS earlier than 9.0, tapping a universal link to your website opens the link in Safari.

“How to support Universal Links?”

  • Step1:创建一个json 格式的apple-app-site-associatio 文件如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "applinks": {
    "apps": [],
    "details": [
    {
    "appID": "9JA89QQLNQ.com.apple.wwdc",
    "paths": [ "/wwdc/news/", "/videos/wwdc/2015/*" ]
    },
    {
    "appID": "TeamID.BundleID2",
    "paths": [ "*" ]
    }
    ]
    }
    }

根据 paths 键设定允许的路径列表, 或只是一个星号如果你想打开 APP 而不管路径是 什么

注意:paths 路径是大小写敏感的

NOTE:The website paths you specify in the paths array are case sensitive.”

“appID”组成部分:TeamID + BundleId TeamID可以从苹果开发账号页面也“Your Account”下查看,BundleId就直接在工程里看了

  • Step2:上传 apple-app-site-association 文件

注意:

1、上传到web server根目录下

2、web server 需要支持https,客户端需要通告https访问,并且不支持任何重定向

upload it to the root of your HTTPS web server. The file needs to be accessible via HTTPS—without any redirects—at https:///apple-app-site-association. Next, you need to handle universal links in your app.

  • Step3:在 APP 里处理通用链接

    • 1、添加域名到 Capabilities

在 Xcode 的 capabilities 里 添加你的 APP 域名, 必须用 applinks: 前置它 这将使APP从上门的域名请求Step2中创建的JSON 文件 apple-app-site-association。当你第一次启动 APP,它会从 https://domain.com/apple-app-site-association 下载这个文件。

* 2、在 AppDelegate 里支持通用链接 

实现: - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler

方法,如下:

当 userActivity 是 NSUserActivityTypeBrowsingWeb 类型, 则意味着它已经由通用链接 API 代理。这样的话, 它保证用户打开的 URL 将有一个非空的 webpageURL 属性

apple 官网地址

通用链接 Universal Links

iOS 视频截图(hls ,mp4)

#HLS截图
From: http://www.jianshu.com/p/bd30ce34a76f

常规获取MP4某一帧画面的方法对m3u8流媒体而言并不适用,如果有需求中需要获取m3u8流媒体截图的小伙伴可以看一下这个方法。

首先,用AVPlayer创建一个视频播放器,并且对AVPlayerItem添加观察者,具体代码不在这里啰嗦了。

流媒体的某一帧在流媒体播放时才能获取,所以我们在KVO中去调用获取画面的方法:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    AVPlayerItem *playerItem=object;
    if ([keyPath isEqualToString:@"status"]) {
        AVPlayerStatus status= [[change objectForKey:@"new"] intValue];
        if(status==AVPlayerStatusReadyToPlay){
            NSLog(@"正在播放...,视频总长度:%.2f",CMTimeGetSeconds(playerItem.duration));
            //image即为获取到的视频帧截图
            UIImage *image = [self getPixelBufferForItem:playerItem];
        }
    }
}

获取视频帧的代码:

//获取m3u8视频帧画面
- (UIImage *)getPixelBufferForItem:(AVPlayerItem *)playerItem{

    AVPlayerItemVideoOutput *output = [[AVPlayerItemVideoOutput alloc] init];
    [playerItem addOutput:output];
    CVPixelBufferRef ref =[output copyPixelBufferForItemTime:CMTimeMake(1000, 60) itemTimeForDisplay:nil];
    UIImage *image = [self CVImageToUIImage:ref];
    return image;
}

copyPixelBufferForItemTime: itemTimeForDisplay:这个方法获取到的视频帧是CVPixelBufferRef类型,我们需要将CVPixelBufferRef类型转化为UIImage类型

//CVPixelBufferRef转UIImage
- (UIImage *)CVImageToUIImage:(CVPixelBufferRef)imageBuffer{
    CVPixelBufferLockBaseAddress(imageBuffer, 0);
    void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
    size_t width = CVPixelBufferGetWidth(imageBuffer);
    size_t height = CVPixelBufferGetHeight(imageBuffer);
    size_t bufferSize = CVPixelBufferGetDataSize(imageBuffer);
    size_t bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);

    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, baseAddress, bufferSize, NULL);

    CGImageRef cgImage = CGImageCreate(width, height, 8, 32, bytesPerRow, rgbColorSpace, kCGImageAlphaNoneSkipFirst|kCGBitmapByteOrder32Little, provider, NULL, true, kCGRenderingIntentDefault);


    UIImage *image = [UIImage imageWithCGImage:cgImage];

    CGImageRelease(cgImage);
    CGDataProviderRelease(provider);
    CGColorSpaceRelease(rgbColorSpace);

    NSData* imageData = UIImageJPEGRepresentation(image, 1.0);
    image = [UIImage imageWithData:imageData];
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
    return image;

}

swift 4 逻辑更完善

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//支持m3u8
open func snapshotImage() -> UIImage? {
guard let playerItem = self.playerItem else { //playerItem is AVPlayerItem
return nil
}
if self.videoOutput == nil {
self.videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: nil)
playerItem.remove(self.videoOutput!)
playerItem.add(self.videoOutput!)
}
guard let videoOutput = self.videoOutput else {
return nil
}
let time = videoOutput.itemTime(forHostTime: CACurrentMediaTime())
if videoOutput.hasNewPixelBuffer(forItemTime: time) {
let lastSnapshotPixelBuffer = videoOutput.copyPixelBuffer(forItemTime: time, itemTimeForDisplay: nil)
if lastSnapshotPixelBuffer != nil {
let ciImage = CIImage(cvPixelBuffer: lastSnapshotPixelBuffer!)
let context = CIContext(options: nil)
let rect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(CVPixelBufferGetWidth(lastSnapshotPixelBuffer!)), height: CGFloat(CVPixelBufferGetHeight(lastSnapshotPixelBuffer!)))
let cgImage = context.createCGImage(ciImage, from: rect)
if cgImage != nil {
return UIImage(cgImage: cgImage!)
}
}
}
return nil
}

相关资料:
使用AVPlayer播放m3u8视频时,实现视频截图

AVPlayer 对m3u8 视频截屏
AVPlayer 截屏
一个git 上的播放器里面有截屏函数


#MP4 截屏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 根据视频的路径获取单帧图片 (多帧数 也有类似的简单函数)
- (UIImage *)getThumbImage:(NSURL *)url {
//测试视频地址
NSURL *soundFileURL = nil;
NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:@"jiangwan" ofType:@"mp4"];
if (soundFilePath.length) {
soundFileURL = [[NSURL alloc] initFileURLWithPath:soundFilePath];
}
//截第一帧
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:soundFileURL options:nil];
AVAssetImageGenerator *gen = [[AVAssetImageGenerator alloc] initWithAsset:asset];
gen.appliesPreferredTrackTransform = YES;
CMTime time = CMTimeMakeWithSeconds(0.0, 1);
NSError *error = nil;
CMTime actualTime;
CGImageRef image = [gen copyCGImageAtTime:time actualTime:&actualTime error:&error];
UIImage *thumb = [[UIImage alloc] initWithCGImage:image];
return thumb;
}

相关链接
http://blog.csdn.net/u013756604/article/details/54095907

http://blog.csdn.net/u013282507/article/details/53930947

FMDB使用札记

From: http://vin-zhou.github.io/2016/11/18/FMDB%E4%BD%BF%E7%94%A8%E6%9C%AD%E8%AE%B0/

文章目录

    1. 前言
    1. 安装
    1. 使用
    1. 创建数据库
    1. 打开数据库
    2. 5.1. 执行更新
    1. 执行查询
    1. 关闭数据库
    1. 事务
    1. 使用线程安全的FMDatabaseQueue
    1. 参考

前言

[FMDB][1] 是一款使用Objective-C对SQLite进行封装的优秀的第三方框架,加上了面向对象的思想。

[1]:

优点:

  • 使用起来更加面向对象,省去了很多麻烦、冗余的C语言代码
  • 对比苹果自带的CoreData框架,更加轻量级和灵活
  • 提供多线程安全,有效地防止数据混乱,原来的SQLite不是线程安全的
  • 支持使用Swift调用

缺点:

  • 因为是OC语言封装的,失去了SQLite原来的跨平台性

FMDB的方方面面在Github上已经交代的比较明白,这里自己再总结摘要一下。

安装

  • 在Github上下载FMBDB源码,将文件拖入工程
  • 在项目target的Build Phases->Link Bianry With Libraries中添加libsqlite3.tbd依赖库
  • #import “FMDatabase.h”

使用

主要包含如下三个类:

  • FMDatabase - 代表一个SQLite数据库,用来在单一线程中执行SQL语句,线程不安全。
  • FMResultSet - 代表执行一次查询后的结果。
  • FMDatabaseQueue - 用于多线程操作数据库的查询和更新,线程安全。

创建数据库

支持传入3种文件路径:

  • 传入真实的文件系统路径,如不存在该文件,将自动创建;
  • 传入空字符串@””. 创建一个本地的临时数据库,当FMDatabase连接关掉后将被自动删除;
  • 传入NULL.创建一个in-memory数据库,当FMDatabase连接关掉后将被自动释放。
1
2
3
4
5
6
7
8
NSArray* array = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* documents = [array objectAtIndex:0];
NSString* path = [documents stringByAppendingPathComponent:@"test.db"];
FMDatabase* db = [FMDatabase databaseWithPath:path];

打开数据库

在使用之前,必须先调用open函数,保证数据库被打开。当资源不足或权限不够时,可能会open失败,这时需要返回,无法后续操作。

1
2
3
4
5
6
7
if (![db open]) {
db = nil;
return;
}

执行更新

任何不以SELECT开头的SQL操作都属于更新操作,包括 CREATE, UPDATE, INSERT, ALTER, COMMIT, BEGIN, DETACH, DELETE, DROP, END, EXPLAIN, VACUUM, 以及 REPLACE 等,通过-executeUpate...这种方法来执行,执行更新正确会返回YES,否则返回NO。

1
2
3
4
5
BOOL updateResult = [db executeUpdate:@"CREATE TABLE myTable (num integer, name varchar(7), sex char(1), primary key(num))"];
if (!updateResult) {return;}
updateResult = [db executeUpdate:@"INSERT INTO myTable(num, name, sex) values(?,?,?)", @0, @"hha", @"m"];

执行查询

一个SELECT查询语句将通过-executeQuery...这种方法来执行,如果正确,返回一个FMResultSet对象,否则返回nil。必须使用-lastErrorMessage和-lastErrorCode方法来查询为何执行失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
FMResultSet* result = [db executeQuery:@"SELECT * FROM myTable"];
NSMutableArray *array = [NSMutableArray array];
while ([result next]) {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
int num = [result intForColumn:@"num"];
NSString *name = [result stringForColumn:@"name"];
NSString *sex = [result stringForColumn:@"sex"];
dict[@"num"] = @(num);
dict[@"name"] = name;
dict[@"sex"] = sex;
[array addObject:dict];
}

关闭数据库

调用close方法即可。

事务

事务,是指作为单个逻辑工作单元执行的一系列操作,要么完整地执行,要么完全地不执行。
想象一个场景,比如你要更新数据库的大量数据,我们需要确保所有的数据更新成功,才采取这种更新方案,如果在更新期间出现错误,就不能采取这种更新方案了,如果我们不使用事务,我们的更新操作直接对每个记录生效,万一遇到更新错误,已经更新的数据怎么办?难道我们要一个一个去找出来修改回来吗?怎么知道原来的数据是怎么样的呢?这个时候就需要使用事务实现。

SQLite进行事务处理:

  • 只要在执行SQL语句前加上以下的SQL语句,就可以使用事务功能了:
  • 开启事务的SQL语句,”begin transaction;”
  • 进行提交的SQL语句,”commit transaction;”
  • 进行回滚的SQL语句,”rollback transaction;”

FMDatabase使用事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
-(void)transaction {
[self.database beginTransaction];
BOOL isRollBack = NO;
@try {
for (int i = 0; i<500; i++) {
NSNumber *num = @(i+1);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [database executeUpdate:sql,num,name,sex];
if ( !result ) {
NSLog(@"插入失败!");
return;
}
}
}
@catch (NSException *exception) {
isRollBack = YES;
[self.database rollback];
}
@finally {
if (!isRollBack) {
[self.database commit];
}
}
}

使用线程安全的FMDatabaseQueue

不能在线程间共用一个FMDatabase, 否则会造成数据混乱! 如果需要使用多线程,应该使用线程安全的FMDatabaseQueue. (#import “FMDatabaseQueue.h” 即可)
FMDatabaseQueue的操作与FMDatabase非常类似,如

  • 创建
    FMDatabaseQueue* queue = [FMDatabaseQueue databaseQueueWithPath: aPath];
  • 操作

将FMDatabase中的操作放到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
[queue inDatabase:^(FMdatabase*db){
}];
即可。
[queue inDatabase:^(FMDatabase*db) {
NSString *sqlStr = @"insert into mytable(num,name,sex) values(4,'xiaoming','m');";
BOOL result = [db executeUpdate:sqlStr];
if (!result) {
NSLog(@"error when insert into database table");
[db close];
}
}];
* 事务
- (void)transactionByQueue {
[self.queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
for (int i = 0; i<500; i++) {
NSNumber *num = @(i+1);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [db executeUpdate:sql,num,name,sex];
if ( !result ) {
*rollback = YES;
return;
}
}
}];
}

参考

iOS学习笔记17-FMDB你好!