美文网首页iOS
MVVM终结者(三)

MVVM终结者(三)

作者: 洪小倲 | 来源:发表于2016-03-03 13:13 被阅读178次

signal的Operations

signal的Operations或者称它是signal的运算与操作,对signal的操作其实是定义在signal的父类RACStream中的,RACStream是一个抽象类,描述了值的流动,不过我至今没有直接用过RACStream,人家都叫抽象类了,非常抽象,还去理解它干嘛。

就跟数字的运算一样,运算过后会生成一个新的数字,signal的运算过后会生成一个新的信号,下面就列举一些signal的运算介绍一下用法

  • filter:(过滤)--返回一个过滤过原本值流动内容的signal
[signal filter:^(NSString *newName) { 
    return [newName hasPrefix:@"h"]; 
}];// 返回一个新的signal,这个signal中的值流动只有h开头的字符串
  • map:(映射)--返回一个值流动内容的映射为内容的signal
[signal map:^(NSString *value) { 
    return [NSString stringWithFormat:@"abner%@",value]; 
}];// 返回一个新的signal,这个signal中的值流动为原来字符串值的前面拼接上abner
  • ignore:(忽略)--返回一个新的信号,这个信号传递的值流动是原来值流动忽略掉指定值的值流动
[signal ignore:nil];// 返回一个新的忽略掉原来nil值的信号
  • merge:(合并)--把多个信号中传递的值流动合并在一个信号中流动,会不会有人在问“那这些值出来的顺序是什么样的?”,我在刚开始接触RAC的时候也掉过这个坑,为什么叫值流动而不叫值,值在时间轴下的走向才叫值流动,这些待合并的信号依赖的是同一个时间轴,先流出来的值在新的信号中也是先流出来,想象成几个水管,把这几个水管接到了一个接口出来,水流的速度都是一样的,先开水龙头的水管的水肯定是先出来的。现在回到前面一篇文章提出的UITextField的两个信号“揉”在一起的问题,结合下面的代码例子理解理解
 [[RACSignal merge:@[self.nameField.rac_textSignal,
                      RACObserve(self.nameField, text)]] 
    subscribeNext:^(NSString* text){ 
      // do something 
   }];

不论上面哪个信号有值流动,新的信号都会有值流动

  • combineLatest:(结合)--结合多个信号,当其中一个信号有值流动时,取所有信号最新的值流动作为新的信号的值流动,注意这里会取所有信号的最新值,也就是说所有的信号必须都得有过值流动(包括空值的流动),如果其中一个信号从未又过值流动则新的信号也不会有值流动,所有的信号中的值流动都是有序而且不能多个值绑在一起流动,而这里的多个值到底是如何绑在一起流动的呢?彻底理解这个先想想swift中的函数是如何返回多个值的,其实原理跟这个一样,先把多个返回值封装成一个返回值(元组)然后返回,在RAC中叫RACTuple,combineLatest:产生的新信号中的值流动就是RACTuple。

  • reduceEach: --针对值流动时RACTuple的信号的,新信号中值流动是根据RACTuple中的数据返回的一个新值,其实这个操作就是个map操作,那为什么要把这么不重要的东西提出来说,因为combineLatest:reduceEach:经常组合起来使用,先combineLatest:然后reduceEach:就可以把多个信号中的最新值流动变成一个值流动的新信号,当然这种官配RAC肯定也给我们封装好了

RAC(self, createEnabled) = [RACSignal  combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ]  
      reduce:^(NSString *password, NSString *passwordConfirm) { 
      return @([passwordConfirm isEqualToString:password]); 
}];

可能有点超纲,我稍微解释一下,等号后面的信号当password和passwordConfirmation的值一样的时候才会有@YES的值流动,等号左边宏RAC的作用是把createEnabled这个属性的值和右边信号中的值流动单向的绑定在一起,右边信号中一旦有值流动会自动赋值给createEnabled属性,相信上面代码的应用场景你已经知晓了现在,把createEnabled改成button的enable属性是不是就能实现以前挺多代码才能实现的逻辑。

  • flatten --只能作用在signal of signals中,signal of signals就是一种值流动是signal的signal。产生的新信号是merge了里面所有的信号。

  • flatmap: --flatmap:传入一个block,这个block需要返回一个signal,每次原来的信号有值流动就会重新执行这个block,所以这里存在很多个return回去的signal,这个运算产生的新的信号中的值流动是meger这些signal以后的值流动。也可以解释为先把普通的signal通过map:变成signal of signals然后再flatten,虽然可以这么解释当时RAC实现起来是先有flatmap:然后flatten其实是封装调用了flatmap的。

[[[signal  
flattenMap:^(User *user) {  
    //上面的信号出正确的值流动的时候会执行这个block 
    return [client loadCachedMessagesForUser:user]; // 这里返回一个信号 }]  
subscribeNext:^(NSArray *newMessages) {  
    //这里的值流动是第二个信号中的值流动 
    NSLog(@"New messages: %@", newMessages);  
} completed:^{  
    NSLog(@"Fetched all messages.");  
}]; 

上面这段代码可以理解为:这个运算产生的新的信号中的值流动是block中return回去的那些信号的值流动,而block中的信号的创建往往要依赖于原信号中吐出来的值,也就是这里其实是有个先后顺序的--原信号产生一个值流动,然后block中根据这个值流动创建一个信号并把这个信号中的值流动添加到运算过后最终的信号里面。原信号再次产生值流动会重复上面的逻辑。假设如果原信号只会吐一次数据就结束了(比如说网络请求signal),那么这个运算的就正好可以处理连接两个有顺序要求的信号。

  • concat:(连接) --真正的连接在这个地方,产生一个新的信号,这个新信号只有等原信号发出了complete标示了以后然后才执行第二个信号(实际上是执行信号被订阅时候的发送数据给订阅者的block),第二个信号中的值流动会出现在新信号中。很简单的就不贴例子了。

  • then: -- then这个其实很好理解,两个有先后顺序的信号的拼接组合,其实then是封装了一下的concat,只是把第一个信号的值流动都给ignore掉,不对第二个信号产生任何的影响。

  • distinctUntilChanged--新信号的值流动中相邻两个值是不一样,原信号中如果相邻两个值是一样的则第二个值流动不会加入到新信号值流动中去

  • doNext: --传入一个block,这个block会在这个新信号被subscribeNext:之前被执行,不要把这个运算理解成接下来要做什么,之前我看这个英文掉过一次坑。

  • deliverOn: --参数为RACScheduler类的对象scheduler,这个方法会返回一个新Signal,针对这个signal的的所有事件都会传递给scheduler参数所表示的线程上执行,而以前管道上的副作用还会在以前的线程上。这个方法主要是切换线程。

  • subscribeOn: --拥有deliverOn:的功能,并且这个运算还会把原信号之前的事件传递到响应的线程中去执行。

  • throttle: --它接收一个时间间隔interval作为参数,如果Signal发出的next事件之后interval时间内不再发出next事件,那么它返回的Signal会将这个next事件发出。也就是说,这个方法会将发送比较频繁的next事件舍弃,只保留一段“静默”时间之前的那个next事件,这个方法常用于处理输入框等信号(用户打字很快),因为它只保留用户最后输入的文字并返回一个新的Signal,将最后的文字作为next事件参数发出。

  • switchToLatest: -- 作用是在signal of signals中,产生的新信号的值流动时原信号中流动的最新的一个signal的值流动,通俗的讲就是切换到里面最新的一个的意思。

  • takeUntil: --返回的新信号跟原信号的值流动是一样的,不过当传入的信号sendNext时,新的signal就sendCompleted,也就是就不继续发送数据了。

[[self signInSignal] takeUntil:self.cancelSignals]; // 登入信号会在取消信号流出@YES的时候取消掉。
  • and、or、not NSNumber中Bool的与、或、非操作,将Signal发出的事件内容转化。

cancat flatmap then 区别

这三个运算的功能貌似很接近,所以有必要单独拿出来区分一下,首先上面我也提到的cancat:then:基本上是一个东西,只是then:把原signal的值流动都ignore掉从而不对后面的signal产生任何的影响,也就是then只是单纯的表示这两个signal的先后顺序。

终于把signal的运算也给写完了,貌似看到这里就可以开始用MVVM+RAC来重构你手中的代码了,接来来贴一些具体的应用场景来理论与实践相结合一下。

map + switchToLatest

如果把这两个结合起来就有意思了,想象这么个场景,当用户在搜索框输入文字时,需要通过网络请求返回相应的hints,每当文字有变动时,需要取消上一次的请求,就可以使用这个配搭。这里简单演示一下

NSArray *pins = @[@172230988, @172230947, @172230899, @172230777, @172230707];
__block NSInteger index = 0;
RACSignal *signal = [[[[RACSignal interval:0.1 
                    onScheduler:[RACScheduler scheduler]] 
                    take:pins.count] 
                    map:^id(id value) { 
                      return [[[HBAPIManager sharedManager] fetchPinWithPinID:[pins[index++] intValue]] 
                              doNext:^(id x) { 
                                  NSLog(@"这里只会执行一次"); 
                              }]; 
                      }] switchToLatest];

[signal subscribeNext:^(HBPin *pin) { 
      NSLog(@"pinID:%d", pin.pinID);} completed:^{ 
      NSLog(@"completed");
}];
// output// 2014-06-05 17:40:49.851 这里只会执行一次
// 2014-06-05 17:40:49.851 pinID:172230707
// 2014-06-05 17:40:49.851 completed

takeUntil的使用来解决cell被复用后产生的问题

[[[cell.detailButton rac_signalForControlEvents:UIControlEventTouchUpInside] 
                      takeUntil:cell.rac_prepareForReuseSignal] 
                      subscribeNext:^(id x) { 
                          // generate and push ViewController
                      }];

cell一旦被复用,那么这个监听就接触了。如果不加takeUntil:cell.rac_prepareForReuseSignal,那么每次Cell被重用时,该button都会被addTarget:selector。

检查本地缓存,如果失效则去请求网络数据并缓存到本地

//创建一个信号,如果缓存还有效则值流动为缓存,如果缓存失效了则流出去一个缓存失效的错误。
- (RACSignal *)loadData { 
    return [[RACSignal 
              createSignal:^(id<RACSubscriber> subscriber) { 
                if (self.cacheValid) { // 缓存有效
                  [subscriber sendNext:self.cachedData]; 
                  [subscriber sendCompleted]; 
                } else { // 缓存无效
                  [subscriber sendError:self.staleCacheError]; 
                } 
               }
              ] subscribeOn:[RACScheduler scheduler]
            ];
}

- (void)update { 
    [[[[self loadData] 
                catch:^(NSError *error) { // 如果本地缓存失效的话
                    return [[self updateCachedData] // 重新获取数据
                                  doNext:^(id data) { 
                                      [self cacheData:data]; // 缓存数据
                                      [self update];//再次调用自己来更新UI 
                                  }
                           ]; 
     }] deliverOn:RACScheduler.mainThreadScheduler] 
    subscribeNext:^(id data) { 
          // 获取有效的数据,更新UI
    }]; 
}

动态检查用户名是否可用

Paste_Image.png

可以看到这里也使用了map + switchToLatest模式,这样就可以自动取消上一次的网络请求。startWith是设置一个signal的初始值流动。

token过期后自动获取新的

下一篇开始用MVVM搭建IOS程序的过程中一些连接的纽带,我看见网上也少有介绍这些的文章,导致真正用MVVM搭建项目的时候发现有些地方衔接不上,比如说界面之间的跳转是属于逻辑层,只有在逻辑的处理过程中才知道什么时候该跳转以及跳转到什么地方去,而在逻辑层VM中是不能拥有VIEW的句柄的,那VM到底是如何跳转的呢?这个问题等到下一篇文章再书。
未完待续

相关文章

网友评论

    本文标题:MVVM终结者(三)

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