ReactiveCocoaをかじってみた

  • 171
    Like
  • 1
    Comment
More than 1 year has passed since last update.

はじめに

最近ReactiveCocoaというものを知りました。これはリアクティブプログラミングというパラダイムのCocoa実装のようです。リアクティブプログラミングそのものはなぜリアクティブプログラミングは重要か。を読むと、分かったような分からないようなもやもやした状態になります。
もやもやした状態は、自分で実験していく事で分かるかもしれません。なので手を動かしてReactiveCocoaをかじってみます。

試しに作るもの

ReactiveCocoa : NSHipsterのサンプルを見つつ、教科書的なユーザ作成画面を作ってみます。

  • username email password passwordVerification のTextField
  • createボタンは以下の条件を満たしたら押せる(ルール1)
    • username,emailが空でない
    • passwordとpasswordVerificationが同じ
    • passwordは8文字以上
  • passwordとpasswordVerificationが違う場合とpasswordが8文字以下の場合は、Labelに警告が表示される(ルール2)

RACsample.gif

Target/Action方式で実装してみる

まずは、作り慣れたTarget/Actionの方法で実装してみます。ViewControllerに、ボタンのenableを操作するupdateButtonEnable:メソッドと、Labelを書き換えるupdatePasswordMessage:メソッドを実装します。また、それぞれのTextFieldにaddTargetしています。


- (void)viewDidLoad
{
    [super viewDidLoad];

    // ルール1
    [self.usernameField addTarget:self
                           action:@selector(updateButtonEnable:)
                 forControlEvents:UIControlEventEditingChanged];
    [self.emailField addTarget:self
                        action:@selector(updateButtonEnable:)
              forControlEvents:UIControlEventEditingChanged];
    [self.passwordField addTarget:self
                           action:@selector(updateButtonEnable:)
                 forControlEvents:UIControlEventEditingChanged];
    [self.passwordVerificationField addTarget:self
                                       action:@selector(updateButtonEnable:)
                             forControlEvents:UIControlEventEditingChanged];

    // ルール2
    [self.passwordField addTarget:self
                           action:@selector(updatePasswordMessage:)
                 forControlEvents:UIControlEventEditingChanged];
    [self.passwordVerificationField addTarget:self
                                       action:@selector(updatePasswordMessage:)
                             forControlEvents:UIControlEventEditingChanged];

}



// ルール1 createボタンのenable条件
-(void)updateButtonEnable:(UITextField *)field
{
    NSLog( @"%@,%@,%@,%@",
          self.usernameField.text,
          self.emailField.text,
          self.passwordField.text,
          self.passwordVerificationField.text);

    self.createButton.enabled =
    [self.usernameField.text length] > 0 &&
    [self.emailField.text length] > 0 &&
    [self.passwordField.text length] >= 8 &&
    [self.passwordField.text isEqual:self.passwordVerificationField.text];

}

// ルール2 passwordの条件
-(void)updatePasswordMessage:(UITextField *)field
{
    NSString *message = @"";
    if( [self.passwordField.text length] < 8){
        message = @"need 8 char.";
    }
    if( ![self.passwordField.text isEqualToString:self.passwordVerificationField.text] ){
        message = @"need same password";
    }
    self.passwordMessageLabel.text = message;
}


絵にするとこんな感じです。
RAC-2.png

ReactiveCocoa方式で実装してみる

次はReactiveCocoaで実装します。Target/Action方式の時に作ったメソッドを、ReactiveCocoaではRACSignalオブジェクトとして実装します。またaddTargetする代わりにRAC()マクロを使ってバインドします。


- (void)viewDidLoad
{
    [super viewDidLoad];

    // ルール1 createボタンのenable条件
    RACSignal *formValidSignal =
    [RACSignal combineLatest:@[ self.usernameField.rac_textSignal,
                                self.emailField.rac_textSignal,
                                self.passwordField.rac_textSignal,
                                self.passwordVerificationField.rac_textSignal]
                      reduce:^(NSString *username,
                               NSString *email,
                               NSString *password,
                               NSString *passwordVerification) {
                          NSLog( @"%@,%@,%@,%@",
                                username,
                                email,
                                password,
                                passwordVerification);

                          return @([username length] > 0 &&
                          [email length] > 0 &&
                          [password length] >= 8 &&
                          [password isEqual:passwordVerification]);
                      }];

    RAC(self.createButton,enabled) = formValidSignal;

    // ルール2 passwordの条件
    RACSignal *passwordValidSignal =
    [RACSignal combineLatest:@[ self.passwordField.rac_textSignal,
                                self.passwordVerificationField.rac_textSignal]
                      reduce:^(NSString *password,
                               NSString *passwordVerification) {
                          NSString *message = @"";
                          if( [password length] < 8){
                              message = @"need 8 char.";
                          }
                          if( ![password isEqualToString:passwordVerification] ){
                              message = @"need same password";
                          }
                          return message;
                      }];
    RAC(self.passwordMessageLabel,text) = passwordValidSignal;
}


絵にすると、Target/Action方式とほぼ同じです。
RAC-2.png

Target/Action方式とReactiveCocoa方式を見比べる

ReactiveCocoa方式では、UITextFieldにrac_textSignaleというプロパティが生えています。またRACSignalのreduce:の戻り値が、RAC()マクロでバインドされたプロパティに値をセットするようです。
ルールの実装が、Target/Action方式では「メソッドに実装」されており、ReactiveCocoaでは「オブジェクトに実装」されているように見えます。
Target/Action方式では、ルールにメソッドの中から直接UIのオブジェクトにアクセスしています。これは「ルール部分とUI部分が癒着している」ようにも見えます。ルールが複雑になると、ここが肥大化して行くのだろうなぁと思います。
一方ReactiveCocoa方式では、ルールがRACSignalとして独立しており、UIとの分離が行われています。ただソースコードの煩雑さが増しています。手放しで喜べるものでもないですね。

RACSignalの連鎖

RACSignalは連鎖する事が出来ます。
ここまでのサンプルでは、ルール1と2に、「パスワードが8文字以上」「パスワードが同じか」のロジックが重複して書かれています。これをRACSignalの連鎖を使って分離します。
RACSignalで拾える「状態変化」は、KVOやUIのイベントNSNotification等いろいろ拾えるようですが、RACSignalそのものも拾えます。なので「パスワードが8文字以上かどうか」というRACSignalと、「パスワードが同じかどうか」というRACSignalを作って、連鎖させてみます。

ややこしいのでまずは絵を見せます。
RAC-2.png


- (void)viewDidLoad
{
    [super viewDidLoad];

    RACSignal *samePasswordSignal =
    [RACSignal combineLatest:@[self.passwordField.rac_textSignal,
                               self.passwordVerificationField.rac_textSignal]
                      reduce:^(NSString *password,
                               NSString *passwordVerification){
                          return @([password isEqualToString:passwordVerification]);
                      }];

    RACSignal *passwordLengthSignal =
    [RACSignal combineLatest:@[self.passwordField.rac_textSignal]
                      reduce:^(NSString *password){
                          return @([password length]>=8);
                      }];

    // ルール1 createボタンのenable条件
    RACSignal *formValidSignal =
    [RACSignal combineLatest: @[self.usernameField.rac_textSignal,
                                self.emailField.rac_textSignal,
                                samePasswordSignal,
                                passwordLengthSignal]
                      reduce:^(NSString *username,
                               NSString *email,
                               NSNumber *passwordsame,
                               NSNumber *passwordLength) {
                          NSLog( @"%@,%@,%@,%@",
                                username,
                                email,
                                passwordsame,
                                passwordLength);
                          return @([username length] > 0 &&
                          [email length] > 0 &&
                          [passwordsame boolValue] &&
                          [passwordLength boolValue]);
                      }];

    RAC(self.createButton,enabled) = formValidSignal;

    // ルール2 passwordの条件
    RACSignal *passwordValidSignal =
    [RACSignal combineLatest:@[samePasswordSignal,
                               passwordLengthSignal]
                      reduce:^(NSNumber *same,
                               NSNumber *length){
                          NSString *message = @"";
                          if( ![length boolValue]){
                              message = @"need 8 char.";
                          }
                          if( ![same boolValue] ){
                              message = @"need same password";
                          }
                          return message;
                      }];
    RAC(self.passwordMessageLabel,text) = passwordValidSignal;
}

RACSignalの生成時に、combineLatestに別のRACSignalを設定しておけば、そのRACSignalにも反応するため、Signal→Signal→ と連鎖させる事が出来ます。
BOOL型で値を返そうとしてもNSBoolといったオブジェクトが無いので、NSNumblerのboolValueを使っています。

この、Signalが連鎖していく所は、通常のObjective-Cで書く場合に実装しにくい所だと思います。また、Signalの連鎖をfilterで止めたりする事も出来るようです。まだ自分はそこまで理解できてませんが。

さいごに

ReactiveCocoaは「リアクティブプログラミング」というパラダイムの1実装のようです。このパラダイムは「オブジェクト指向プログラミング」や「関数型プログラミング」と同じレベルの概念で、正直難しくて分かっていません。ですが「関係性を記述する」という考え方はとても魅力的です。参照:2010-12-26 なぜリアクティブプログラミングは重要か。

ReactiveCocoaを「便利なライブラリ」として見た場合、「状態の変化をトリガーに、他の状態を書き換える」ために使えます。これはMacOS用アプリのCocoaBindingに似ていると思います。
またSignalの連鎖は、普通に作ったときにはあまり思いつかないアイデアです。UIの「ビジネスロジック」部分が複雑でややこしくなる場合、このアイデアでルールを分解して組み合わせる、という使い方が出来ると思います。

一方でReactiveCocoaは、使うには「複雑」なライブラリだと思います。これは元々の「リアクティブプログラミング」というパラダイムを「ライブラリ」としてObjective-Cに組み込んでいるので、Objective-Cと馴染んでおらず、ソースコードの複雑さが増しているように思います。

今現在は、ReactiveCocoaは「使い所が難しい」というイメージで落ち着いています。

参考にしたページ