ReactiveCocoa 学习笔记(一)

MVVM

  • MVC ---> Model View Controller ----> 复杂应用 ---> Massive View Controller(重量级视图控制器) 。所以需要给给视图控制器瘦身.
  • MVVM 是 MVC 模式的一种演进,主要解决了ViewController过于臃肿带来的不易维护和测试问题。
  • view 是一个壳 ,不关心数据,所呈现的内容都由 ViewController来提供。但是又不是直接去沟通viewmodel,而是用viewController来做中间的协调者。
  • viewController 管理view和viewModel,当VC初始化的时候通过viewModel中的方法去取数据。尽量不涉及业务逻辑,让 ViewModel 去做这些事情.只是一个中间人,接收 View 的事件、调用 ViewModel 的方法、响应 ViewModel 的变化。
  • 好处:可以让Controller更加简单和轻便,而且ViewModel相对独立,也更加方便测试和重用

Controller

  • 可以被看成View
  • 处理布局、动画、接收系统事件、展示UI

View-Model

  • 代表应用程序UI状态的特殊类型的模型
  • 获取原始数据, 运用逻辑, 并处理成 view (controller) 的展示数据.
  • 不能包含 View,不然就跟 View 产生了耦合,不方便复用和测试。
  • 它(通常通过属性)暴露给视图控制器需要知道的仅关于显示视图工作的信息(理想地你不会暴漏你的 data-model 对象). 它还负责对上游数据的修改(比如更新模型/数据库, API POST 调用).
  • 富有逻辑和转换数据并将结果存到属性的功能. 理想上相同的输入(比如网络服务响应)将会导出相同的输出(属性的值). 这意味着尽可能多地消除由”应用世界”剩余部分带来的可能影响输出的因素。
  • ViewModel 之间可以有依赖。比如VC的VM和tableViewCell的VM。
  • ViewModel 避免过于臃肿,不然维护起来也是个问题.
  • 不对视图控制器以任何形式直接起作用或直接通知其变化.

当 view-model 的共有属性发生变化 更新视图方法

  • 用对应的 view-model 来初始化视图控制器

     UIViewController *controller =[[UIViewController alloc] initWithViewModel:  ViewModel];
    
  • 无法在初始化时将 view-model 传入 比如在 storyboard segue 或 cell dequeuing 的情况下,这时需要在视图(控制器)中暴露一个公有可写的 view-model 属性

    UITableViewCell *cell =[self.tableView dequeueReusableCellWithIdentifier: @"cell" forIndexPath: indexPath];
    // 给暴露出来的viewmodl赋值
    cell.viewModel = self.viewModel. tweets[indexPath. row];
    
  • 可以在钩子程序调用前传入 view-model, 比如 init 和 viewDidLoad, 我们可以从view-model 的属性初始化所有 UI 元素的状态.

    //dontDoThis1.m 
    - (id) initWithViewModel:(MYTwitterLookupViewModel *) viewModel {
        self = [super init];
        if (!self) return nil;
        _viewModel = viewModel;
        return self;
    }
    - (void) viewDidLoad {
        [super viewDidLoad];
        _goButton.enabled = viewModel.isUsernameValid;
        _goButton.alpha = viewModel.isUsernameValid ? 1 : 0.5;
        // etc
    }
    
    

MVVM实现方式

  • Delegate
  • KVO
  • Block
  • ReactiveCocoa

MVVM还有一个很重要的概念是 data binding,view的呈现需要data,这个data就是由ViewModel提供的,将view的data与ViewModel的data绑定后,将来双方的数据只要一方有变化,另一方就能收到。这里有Github 开源的一个ViewModel Base Class。

函数响应式式编程

  • Functional Reactive Programming(以下简称FRP)是一种响应变化的编程范式。
  • FRP提供了一种信号机制来实现这样的效果,通过信号来记录值的变化。信号可以被叠加、分割或合并。通过对信号的组合,就不需要去监听某个值或事件。
  • 在 iOS/Objective-C 中达到纯函数水平是很棘手的(Swift 提供了一些附加的函数性, 这会让我们更接近).
  • 简单函数为例子, 它们可能接受一两个参数并输出一个结果. 数据输入, 数据输出.这个函数可能是做一些数学运算或是将姓和名结合到一起. 无论应用的其他地方发生啥, 这个函数总是对相同的输入产生相同的输出. 这就是函数式方面
  • view-model 仍然只是一个对象, 而不用维护一些状态(否则它将不会是你视图中非常好用的模型了. )但你仍该努力将尽可能多的逻辑移到无状态的函数”值”中. 再重复一次, Swift在这方面比 Objective-C 更加可行.
  • 单元测试方便,bug 数量随着状态数递减,避免了视图控制器的复杂性

ReactiveCocoa

  • 在iOS平台上对FRP的实现。
  • 核心:信号,用RACSignal实现。

RACSignal

  • RACSignal是数据流,可以被绑定和传递.
  • RACSignal (信号)是RAC的构造单元,作用就是可以预先运用逻辑并构建处理(陈述性),而不是非要等到事情发生后才处理(命令式)。
  • RACSignal可以把所有这些异步方法(代理、block、通知、KVO、target/action等)统一到一个接口下。[图片上传失败...(image-6223bb-1510888578514)]
  • 订阅后,当订阅者发生变化,RACSignal会向订阅者一次发送0或多个载有数值的“next”事件,后面跟着一个“complete”或“error”事件。

常见关键词

  • createSignal:创建信号
  • subscriber 有了订阅者,传送给订阅者
  • filter:过滤,符合条件才向下执行。
  • defer : 将代码的创建推迟到信号被订阅
  • concat: 连接信号,第一个信号必须发送完成,第二个信号才会被激活
  • map : 修改/映射,将信号内容转换。把信号改成需要的类型。
  • takeUtil : signalA takeUntil:signalB 当signalB激活之后,停止signalA 的订阅
  • combineLatest:reduce:多个信号合成成一个新的信号,只要其中一个有响应,这个新的信号就会得到这个响应。
  • combine:叠加
  • chain:串联
    combineLatest:reduce
RAC(self.myButton, enabled) = [RACSignal combineLatest:@[self.usernameField.rac_textSignal, self.passwordField.rac_textSignal] reduce:^id(NSString *userName, NSString *password) {
    //如果用户名和密码框的长度都超过6,提交按钮就enable
    return @(userName.length >= 6 && password.length >= 6);
}];

RACObserve(TARGET, KEYPATH)

  • 参数:
    • TARGET:要观察的对象
    • KEYPATH: 要观察的那个对象某属性的keypath
  • RACObserve 会创建一个信号, 一旦它有了订阅者, 它就立刻发送那个属性的当前值, 并发送那个属性在这之后的任何变化。
//监听button点击事件
[[_myButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *button){
       NSLog(@"点击了button");
}];
//监听textView.text
[[_myTextFiled rac_textSignal]subscribeNext:^(NSString * _Nullable x) {
       NSLog(@"x---%@",x);
}];

注意: RAC 只处理对象, 而不处理像 BOOL 这样的原始值. 不过不用担心, RAC 通常会帮你这些转换.

  • 宏 RAC(TARGET, ...)与 宏RACObserve(TARGET, KEYPATH) 相同
    • RAC(TARGET, ...) 想要与即将到来的值绑定的对象和参数

转换数据流

  • map 方法:返回的是一个对象。

    //将viewModel的isUsernameValid与myButton的enadbled和backgroundColor绑定。
    RACSignal *userNameIsValidSignal = RACObserve(self.viewModel, isUsernameValid);
    RAC(self.myButton,highlighted) = userNameIsValidSignal;
    RAC(self.myButton,backgroundColor) = [userNameIsValidSignal map:^id _Nullable(NSNumber*  _Nullable value) {
    return value.boolValue ? [UIColor redColor] : [UIColor blueColor];
        }];
    

RAC的大统一


在 RAC 的文档和一些介绍 RAC 的 Keynote 资料里面我们可以看到说 RACSignal 可以来替代 Delegate、 Block Callbacks、Target Action、KVO、Notifications。因为它们本质上都是值的变化(Values over time)。


image.png

KVO
RAC可以用来监测属性的改变,这点跟KVO很像,不过使用了block,而不是-observeValueForKeyPath:ofObject:change:context:

[RACAble(self.username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

使用起来是不是比KVO舒服多了。比KVO更加强大的是信号可以被链起来(chain)

// 只有当名字以'j'开头,才会被记录
[[RACAble(self.username)
   filter:^(NSString *newName) {
       return [newName hasPrefix:@"j"];
   }]
   subscribeNext:^(NSString *newName) {
       NSLog(@"%@", newName);
   }];

UI Event
RAC还为系统UI提供了很多category,来方便消息的创建和传递,比如按钮被点击或文本框有改动等等。如果有subscriber的话,会自动触发。
比如:self.usernametextField.rac_textSignal

Network Request && Async work
这些可以通过自定义信号,也就是RACSubject(继承自RACSignal,可以理解为自由度更高的signal)来搞定.

//比如一个异步网络操作,可以返回一个subject,然后将这个subject绑定到一个subscriber或另一个信号
- (void)doTest
{
    RACSubject *subject = [self doRequest];
    
    [subject subscribeNext:^(NSString *value){
        NSLog(@"value:%@", value);
    }];
}

- (RACSubject *)doRequest
{
    RACSubject *subject = [RACSubject subject];
    // 模拟2秒后得到请求内容
    // 只触发1次
    // 尽管subscribeNext什么也没做,但如果没有的话map是不会执行的
    // subscribeNext就是定义了一个接收体
    [[[[RACSignal interval:2] take:1] map:^id(id _){
        // the value is from url request
        NSString *value = @"content fetched from web";
        [subject sendNext:value];
        return nil;
    }] subscribeNext:^(id _){}];
    return subject;
}
image

Signal and Subscriber

  • signal 和 subscriber 是RAC中最核心的,signal如果比喻成插座的话,subscriber 就是 插头。插座负责去取电,插座负责使用。插头可以无限串联(chain)。当插座signal 什么都不干的时候,也就是处于冷状态,有了插头插才会去获取,这个时候就处于热状态。

  • signal 获取到数据后,会调用subscriber的sendNext,sendComplete,sendError方法来传送数据,如果没有sendComplete和sendError,新的值就会通过sendNext一直传输。

    //使用KVO监听了property的变化,只要username变化,block就会执行。NSURLCache的currentDiskUsage就不能被RACObserve。
    [RACObserve(self, username) subscribeNext: ^(NSString *newName){
        NSLog(@"newName:%@", newName);
    }];
    
  • signal 很灵活,被修改(map),过滤(filter),叠加(combine),串联(chain),这有助于应对更加复杂的情况.

//把登录按钮enable和用户名输入框、密码输入框、loggingIn、self.loggedIn 绑定。如果reduce 输出NO,则登录按钮不能点击,反之则可以点击。返回的值必须是object对象。
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);
        }];

冷信号(Cold)和热信号(Hot)

  • createSignal 创建了信号但是没有订阅,所以此时这个信号处于冷状态。
  • 当创建的信号被订阅后,状态才会变成热状态.

Cocoa Categories
为了更加方便地使用RAC,RAC给Cocoa添加了很多category,与系 统集成地越紧密,使用起来自然也就越方便

  • UIView Categories

    //点击button后会发出信号signal,通过rac_command处理
      @weakify(self)
        self.myButton.rac_command = [[RACCommand alloc] initWithEnabled:self.enable signalBlock:^RACSignal *(id input) {
            // Assume that we're logged in at first. We'll replace this signal later if not.
            RACSignal *authSignal = [RACSignal empty];
            //判断条件是否通过
            if (self.enable) {
                //未通过后的处理
                    return nil;
                };
            return [authSignal then:^RACSignal *{
                @strongify(self);
                //通过后的处理
                return nil;
            }];
        }];
        [self.myButton.rac_command.errors subscribeNext:^(id x) {
            [x subscribeNext:^(NSError *error) {
                //失败处理
            }];
        }];
    
    
  • 常用的数据结构也添加了相应的category。

    //NSArray为例
    NSArray *array = @[@1,@12,@3,@4];
        //有序的不可变集合,是运行时计算的,所以对性能提升有一定的帮助
        RACSequence *sequece = [array rac_sequence];
        //转换成NSArray
        NSArray *testArray =  [sequece array];
        [sequece any:^BOOL(id  _Nullable value) {
            //检查sequece中的每一个元素是否符合要求
        }];
        [sequece all:^BOOL(id  _Nullable value) {
            //检查是不是所有的元素都符合要求
        }];
    
  • 通知(NotificationCenter)
    默认情况下使用 通知 的话需要使用Target-Action方式来处理Notifiction,这样就需要从新定义一个方法,而且还要起名字(编程领域的两大难题之一)防止重名。还需要在合适的时候移除observer。

    //RAC 实现 不用担心observer移除问题。
    [[[NSNotificationCenter defaultCenter] rac_addObserverForName:@"MyNotification" object:nil] subscribeNext:^(NSNotification *notification) {
        NSLog(@"Notification Received");
    }];
    
  • NSObject

    • 一个object的dealloc被触发时,执行的一段代码.《NSObject+RACDeallocating.h》

      ```
      

      NSArray *array = @[@"foo"];
      [[array rac_willDeallocSignal] subscribeCompleted:^{
      NSLog(@"oops, i will be gone");
      }];
      array = nil;

    • 满足一定条件时,自动触发某个方法《NSObject+RACLifting.h》

      //当signalA和signalB都至少sendNext过一次,接下来只要其中任意一个signal有了新的内容,doA:withB这个方法就会自动被触发。
      - (void)test
      {
          RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
              double delayInSeconds = 2.0;
              dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
              dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
                  [subscriber sendNext:@"A"];
              });
              return nil;
          }];
          RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
              [subscriber sendNext:@"B"];
              [subscriber sendNext:@"Another B"];
              [subscriber sendCompleted];
              return nil;
          }];
          [self rac_liftSelector:@selector(doA:withB:) withSignals:signalA, signalB, nil];
      }
      - (void)doA:(NSString *)A withB:(NSString *)B
      {
          NSLog(@"A:%@ and B:%@", A, B);
          // A:A and B:Another B
      }
      
  • NSObject+RACSelectorSignal.h

    • rac_signalForSelector:某个事件响应后同时做一个操作。相当于hook.
    • rac_signalForSelector:fromProtocol:selector实现了某个协议,所以可以用它来实现Delegate
      [[self rac_signalForSelector:@selector(test)]subscribeNext:^(RACTuple * _Nullable x) {
            //执行test方法的时候,也把这里执行了。
        }];
        //这个方法主要是把 protocal 转为一个 Signal 便于使用。值得注意的是这个函数返回的是一个 RACTuple。 这个 RACTuple 包含了 Selector 方法里面所有的参数。这样需要用到的时候主要按照顺序来获取。RAC 来替代 Delegate .
        [[self rac_signalForSelector:@selector(test) fromProtocol:(@protocol(UITableViewDelegate))]mapReplace:@YES];
    

小结:

  • 当一个signalsubscriber 订阅后,这个subscriber会在执行到sendCompletesendError移除。也可以手动移除[disposable dispose].当 subscriberdispose后,所有相关subscriber相关的工作都会停止,资源也会释放。
  • Signal evente 是线性的,不会出现并发的情况,除非显示的指定 Scheduler,所以,-subscribeNext:error:completed: 里的block 不需要锁定或者synchronized等操作,其他的events会依次排队,直到block处理完成。
  • Error有优先权,如果多个signal被同时监听,只要有一个signal SendError就会被传送给subscriber,并导致signal全部终止执行,相当于 Exception
  • 生成Signal时,最好指定Name, -setNameWithFormat:方便调试。
  • block代码中不要阻塞。

参考:

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

推荐阅读更多精彩内容