• ReactiveCocoa基本组件:理解和使用RACCommand


    原文地址在这里

    本文源码:https://github.com/olegam/RACCommandExample

    RACCommand

    RACCommand是ReactiveCocoa的基本组件之一,能节省开发的大部分时间,同时使得iOS/OS X 应用更健壮。

           我看到一部分ReactiveCocoa(以下简单RAC)新人并没有完全理解RACCommand,自然也就不知道怎么用它。所以我写了这个小小介绍性文章,希望能对你的理解有所帮助。RACCommand源文件里的注释写得很不错,不过它并没有给任何例子来说说具体怎么用它,对于RAC的新人来说,只看这些注释还是比较难以理解的。

           RACCommand类用于表示事件的执行,一般来说是在UI上的某些动作来触发这些事件,比如点击一个按钮。RACCommand的实例能够决定是否可以被执行,这个特性能反应在UI上,而且它能确保在其不可用时不会被执行。通常,当一个命令可以执行时,会将它的属性allowsConcurrentExecution设置为它的默认值:NO,从而确保在这个命令已经正在执行的时候,不会同时再执行新的操作。命令执行的返回值是一个RACSignal,因此我们能对该返回值进行next:,completed或error:,这在下文会有所展示。

    例子应用

    现在假设我们要写一个简单的iOS APP,它能让用户订阅邮件列表。我们将其做到最简单:一个输入框和一个按钮。当用户输入了电子邮箱地址、点击订阅按钮后,电子邮箱地址会提交到我们的web服务器上。够简单了吧!然而,我们还是得处理一些边界情况,以提供最好的体验。比如如果用户点击按钮两次怎么办?错误如何处理?如果邮箱地址非法呢?RACCommand能帮助我们处理这些情况。我已经实现了一个小的app来演示本文中所讨论的这些概念。

    源码:https://github.com/olegam/RACCommandExample

    本例用了一个非常简单的视图控制器,同样还演示了iOS应用的MVVM模式。

    根视图控制器包含了视图,还有view model的实例。

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. - (void)bindWithViewModel {     
    2.   RAC(self.viewModel, email) =self.emailTextField.rac_textSignal;   
    3.   self.subscribeButton.rac_command = self.viewModel.subscribeCommand;     
    4.   RAC(self.statusLabel, text) =RACObserve(self.viewModel, statusMessage);   
    5. }  

    上面的这个方法(在viewDidLoad中调用)将view以及view model绑定。对于咱们感兴趣的代码在都view model中。看看view model的接口:

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. @interface SubscribeViewModel :NSObject    
    2.   @property(nonatomic, strong)RACCommand *subscribeCommand;  // writeto this property  
    3.   @property(nonatomic, strong) NSString *email;  // read from this property  
    4.   @property(nonatomic, strong) NSString *statusMessage;    
    5. @end  

    RACCommand暴露在这里,就是本文接下来要讨论的。另外还有两个被绑定到view上用于展示的字符串。这个view model的完整实现如下:

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. #import "SubscribeViewModel.h"  
    2. #import "AFHTTPRequestOperationManager+RACSupport.h"  
    3. #import"NSString+EmailAdditions.h"  
    4.    
    5. static NSString *const kSubscribeURL =@"http://reactivetest.apiary.io/subscribers";  
    6.    
    7. @interface SubscribeViewModel ()  
    8. @property(nonatomic, strong) RACSignal*emailValidSignal;  
    9. @end  
    10.    
    11. @implementation SubscribeViewModel  
    12.    
    13. - (id)init {  
    14.        self= [super init];  
    15.        if(self) {  
    16.               [selfmapSubscribeCommandStateToStatusMessage];  
    17.        }  
    18.        returnself;  
    19. }  
    20.    
    21. -(void)mapSubscribeCommandStateToStatusMessage {  
    22.        RACSignal*startedMessageSource = [self.subscribeCommand.executionSignalsmap:^id(RACSignal *subscribeSignal) {  
    23.               returnNSLocalizedString(@"Sending request...", nil);  
    24.        }];  
    25.    
    26.        RACSignal*completedMessageSource = [self.subscribeCommand.executionSignalsflattenMap:^RACStream *(RACSignal *subscribeSignal) {  
    27.               return[[[subscribeSignal materialize] filter:^BOOL(RACEvent *event) {  
    28.                      returnevent.eventType == RACEventTypeCompleted;  
    29.               }]map:^id(id value) {  
    30.                      returnNSLocalizedString(@"Thanks", nil);  
    31.               }];  
    32.        }];  
    33.    
    34.        RACSignal*failedMessageSource = [[self.subscribeCommand.errors subscribeOn:[RACSchedulermainThreadScheduler]] map:^id(NSError *error) {  
    35.               returnNSLocalizedString(@"Error :(", nil);  
    36.        }];  
    37.    
    38.        RAC(self,statusMessage) = [RACSignal merge:@[startedMessageSource,completedMessageSource, failedMessageSource]];  
    39. }  
    40.    
    41. - (RACCommand *)subscribeCommand {  
    42.        if(!_subscribeCommand) {  
    43.               @weakify(self);  
    44.               _subscribeCommand= [[RACCommand alloc] initWithEnabled:self.emailValidSignalsignalBlock:^RACSignal *(id input) {  
    45.                      @strongify(self);  
    46.                      return[SubscribeViewModel postEmail:self.email];  
    47.               }];  
    48.        }  
    49.        return_subscribeCommand;  
    50. }  
    51.    
    52. + (RACSignal *)postEmail:(NSString *)email{  
    53.        AFHTTPRequestOperationManager*manager = [AFHTTPRequestOperationManager manager];  
    54.        manager.requestSerializer= [AFJSONRequestSerializer new];  
    55.        NSDictionary*body = @{@"email": email ?: @""};  
    56.        return[[[manager rac_POST:kSubscribeURL parameters:body] logError] replayLazily];  
    57. }  
    58.    
    59. - (RACSignal *)emailValidSignal {  
    60.        if(!_emailValidSignal) {  
    61.               _emailValidSignal= [RACObserve(self, email) map:^id(NSString *email) {  
    62.                      return@([email isValidEmail]);  
    63.               }];  
    64.        }  
    65.        return_emailValidSignal;  
    66. }  
    67.    
    68. @end  
    69. 呃,这是个大块头,我们一点一点来看。我们最感兴趣的RACCommand的创建如下:  
    70. - (RACCommand *)subscribeCommand {  
    71.        if(!_subscribeCommand) {  
    72.               @weakify(self);  
    73.               _subscribeCommand= [[RACCommand alloc] initWithEnabled:self.emailValidSignalsignalBlock:^RACSignal *(id input) {  
    74.                      @strongify(self);  
    75.                      return[SubscribeViewModel postEmail:self.email];  
    76.               }];  
    77.        }  
    78.        return_subscribeCommand;  
    79. }  

    command的初始化方法中有一个enabledSignal参数,这个signal就是用来指名command能否被执行的。在我们的例子中,当用户输入的email地址合法时,它才能被执行。self.emailValidSignal这个signal每当email的文本更新时,会发送NO或YES。

    signalBlock参数在command需要执行时调用,这个block需要返回一个signal用来表示正在执行,之前将allowsConcurrentExecute的值设置为默认值NO,此时command会观察这个signal,而且在这个执行进度完成前,不允许新的执行。

    由于command是button的rac_command的属性(定义在UIButton+RACCommandSupport),这个button的enable状态会根据command能否执行来自动改变。

    当用户点击按钮时,command会自动执行。如果你需要手动执行command,可以发送消息:-[RACCommand execute:],参数是可选的,在我们的例子中,我们不需要手动执行。尽管在我们这里不用,但它还是很有用的,这个-execute:方法是一个查看命令执行的状态的方法,如下:

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. [[self.viewModel.subscribeCommand execute:nil] subscribeCompleted:^{  
    2.   NSLog(@"Thecommand executed");   
    3. }];  

    在我们的例子中,button自动执行command(所以我们不需要调用-execute:),所以我们得监听command的另一个属性,在command执行时更新UI。在这里有几种选择来达到目的,也许会有点小迷惑。executionSignals是RACCommand的signal,每当command开始执行时next:,其参数是由command创建的signal,所以这个executionSignals是一个值为signal的signal。我们在view model的mapSubscribeCommandStateToStatusMessage方法中,在command每次开始执行时得到一个包含字符串值的signal:

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. RACSignal*startedMessageSource = [self.subscribeCommand.executionSignalsmap:^id(RACSignal *subscribeSignal) {  
    2.   returnNSLocalizedString(@"Sending request...", nil);  
    3. }];  

    如果我们想用纯粹的函数在command执行完成后得到一个包含字符串的signal,我们不得不多做一点小工作:

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. RACSignal *completedMessageSource =[self.subscribeCommand.executionSignals flattenMap:^RACStream *(RACSignal*subscribeSignal) {  
    2.               return[[[subscribeSignal materialize] filter:^BOOL(RACEvent *event) {  
    3.                      returnevent.eventType == RACEventTypeCompleted;  
    4.               }]map:^id(id value) {  
    5.                      returnNSLocalizedString(@"Thanks", nil);  
    6.               }];  
    7.        }];  



    flattenMap:方法在command执行时,会调用block并传入subscribeSignal,这个block会返回一个新的signal,它的值就在这个要返回的signal中。materialize会将一个signal转换为RACEvent信号(将一个signal的next:complete和error:消息转换为RACEvent实例的next:的值)。接下来我们就过滤这些事件,只留下RACEventTypeCompleted完成事件,并将其map成一个字符串值。看懂了没?如果没看懂的话,你最好看一下flattenMap:materialize:是用来做什么的。

    我们可以用一个不同的方法来实现上面这种行为,还可能更容易懂:

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. @weakify(self);  
    2. [self.subscribeCommand.executionSignals subscribeNext:^(RACSignal*subscribeSignal) {    
    3.    [subscribeSignalsubscribeCompleted:^{        
    4.     @strongify(self);       
    5.     self.statusMessage= @"Thanks";    
    6.    }];   
    7. }];  

    我不推荐上面这种方法,因为在block里的是副作用(side effects)。还有,上面的block会referring和retaining self,所以我用了@weakify和@strongify宏(在libextobjc中定义)来避免循环引用。所以为了避免使用这种副作用,还是要尽量采用原始的实现好一些。

    还有一个要重点注意的是executionSignals属性,这个signals不会发送了error事件,而是由errors这个属性来发送的。在一个command的执行期间,如果一个signal发送了error,这会被signals当成一个next:事件发送,而errors属性则会发送这个错误信息。errors属性不会发送error:事件。我们很容易就能将错误信息map成一个字符串消息:

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. RACSignal *failedMessageSource = [[self.subscribeCommand.errorssubscribeOn:[RACScheduler mainThreadScheduler]] map:^id(NSError *error) {  
    2.   returnNSLocalizedString(@"Error :(", nil);  
    3. }];  

    现在我们有3个关于状态的signals了,现在要展示给用户看,我们将这个3个signals合并成一个signal,并将其绑定到view model的statusMessage属性上(这会被绑定到view controller的statusLabel.text属性上)。

    [objc] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. RAC(self, statusMessage) = [RACSignalmerge:@[startedMessageSource, completedMessageSource, failedMessageSource]];  

    以上就是在iOS应用里怎么使用RACCommand的一例子了。我认为这个实现逻辑相比较于在view controller里用UITextFieldDegate实现来说要好得多,因为它不需要各种状态变量和属性。

  • 相关阅读:
    disable_irq与disable_irq_nosync使用场景
    linux中断处理原理分析
    工作队列(workqueue) create_workqueue/schedule_work/queue_work
    使用git建立远程仓库,让别人git clone下来
    C中字符串的几种定义方法及说明
    Linux 2.6内核Makefile浅析
    探究platform_driver中的shutdown用途
    匆匆
    至强CPU性能排行,从X3210起,由低至高排列。
    Linux 命令行快捷键
  • 原文地址:https://www.cnblogs.com/iamjjh/p/4757597.html
走看看 - 开发者的网上家园