iOS
ReactiveCocoa

ReactiveCocoa始めました

More than 3 years have passed since last update.

参考
- ReactiveCocoa/ReactiveCocoa (GitHub)
- ReactiveCocoa Tutorial - The Definitive Introduction
- MVVM Tutorial with ReactiveCocoa
- ReactiveCocoa勉強会関西を開催しました

注意
最近勉強し始めたばかりなので間違いが含まれるかもしれません。
指摘いただけると助かります。

はじめに

FRPとかMVVMの話は理解しきれていないので割愛します。
ReactiveCocoa(以下RAC)を使って(MVVMを意識して)初めてコード書いてみたので、サンプルの説明と感想を書きます。

RACを使ってTwitterのSearchをしてみる

サンプル
- 335g/ReactiveTwitterSearch

READMEにあるバインディング例とかだと簡単すぎてよくわからなかったので、RACを使って(簡易的に)Twitter検索をできるようにしてみました。

サンプルは2つのMVVMからなります。

  • EEEETwitterSearch〜 (検索する)
  • EEEETwitterSearchResult〜 (検索結果を表示する)

検索する (EEEETwitterSearch〜)

バインディング

viewに関するデータをviewModelで管理させるため、viewControllerとviewModel間でバインディングします。

サンプルでは、UISearchBarに入力した文字をviewModelで管理しています。

EEEETwitterSearchViewController.m
RAC(self.viewModel, searchText) = [[self rac_signalForSelector:@selector(searchBar:textDidChange:)
                                                  fromProtocol:@protocol(UISearchBarDelegate)]
                                        map:^id (RACTuple *tuple){
                                            UISearchBar *searchBar = tuple.first;
                                            return searchBar.text;
                                        }];

RAC(object, property)マクロはobject.propertyを監視するためのものであり、RACSignalを代入する事でバインディングが実現します。上記コードではUISearchBarDelegateプロトコルのsearchBar:textDidChange:メソッドを監視するSignalを- rac_signalForSelector:fromProtocol:で作り、引数からUISearchBar.textを抽出しています。これにより、UISearchBarへの入力が行われる度にviewModel.searchTextが同期されます。

イベント処理

イベント処理(検索)にはRACSignalを使います。全体的な流れは

  1. アカウント承認依頼をする - (RACSignal *)requestTwitterAccount
  2. エラーハンドリング (承認されない場合、アカウントが無い場合)
  3. 検索する - (RACSignal *)search
  4. 検索結果を受け取り pushViewControllerする

です。通常これらの処理はdelegateで処理するのが一般的ですが、RACを使う事でblocks処理が可能になります。具体的にサンプルを示すと以下の部分です。

EEEETwitterSearchViewController.m
[[self.viewModel requestTwitterAccount] // 1. アカウント承認依頼をする
      subscribeError:^(NSError *err){
          // 承認得られず or アカウント無い
      } completed:^{
          // 承認通ってアカウント取得できた

          @strongify(self);

          [[[self.viewModel search] // 4. 検索する
            deliverOn:[RACScheduler mainThreadScheduler]] // 以降の処理はメインスレッドで
            subscribeNext:^(NSDictionary *responseData){
                // 検索完了

                @strongify(self);

                EEEETwitterSearchResultViewModel *viewModel;
                viewModel = [[EEEETwitterSearchResultViewModel alloc] initWithSearchResultData:responseData];

                NSString *className = NSStringFromClass([EEEETwitterSearchResultViewController class]);
                UIStoryboard *storyboard = [UIStoryboard storyboardWithName:className bundle:nil];

                EEEETwitterSearchResultViewController *vc;
                vc              = [storyboard instantiateInitialViewController];
                vc.title        = self.viewModel.searchText;
                vc.viewModel    = viewModel;

                [self.navigationController pushViewController:vc animated:YES];

            } error:^(NSError *err){
                // 検索が失敗した
                NSLog(@"error:%@", err);
            }];


      }];

viewに関する処理のみであり、具体的なアカウント承認や検索処理を記述していない事がわかります。実際のアカウント承認はviewModelではなくmodelが行っています。具体的にはEEEETwitterSearchプロトコルの- (RACSignal *)requestAccessToTwitterSignalです。

EEEETwitterSearchServices.m
- (RACSignal *)requestAccessToTwitterSignal {

    if (!self.accountStore) {
        self.accountStore = [[ACAccountStore alloc] init];
    }

    if (!self.accountType) {
        self.accountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
    }

    NSError *accessError = [NSError errorWithDomain:kEEEETwitterSearchServicesInstantDomain
                                               code:EEEETwitterSearchErrorAccessDenied
                                           userInfo:nil];

    @weakify(self);
    RACSignal *requestSignal = [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber){
        @strongify(self);

        [self.accountStore requestAccessToAccountsWithType:self.accountType
                                                   options:nil
                                                completion:^(BOOL granted, NSError *error){

                                                    if (granted) {

                                                        NSError *noAccountErr = [NSError errorWithDomain:kEEEETwitterSearchServicesInstantDomain
                                                                                                    code:EEEETwitterSearchErrorNoTwitterAccounts
                                                                                                userInfo:nil];

                                                        NSArray *accounts = [self.accountStore accountsWithAccountType:self.accountType];
                                                        if (accounts.count == 0) {
                                                            [subscriber sendError:noAccountErr];

                                                        }else {
                                                            self.activeAccount = [accounts firstObject];
                                                            [subscriber sendNext:nil];
                                                            [subscriber sendCompleted];
                                                        }

                                                    }else {
                                                        [subscriber sendError:accessError];
                                                    }
                                                }];
        return nil;
    }];

    return requestSignal;
}

ポイントは- sendNext:- sendError:- sendCompletedの部分です。これによりイベントが連鎖していきます。また引数にデータを渡す事が可能であり、検索の際には検索結果を渡しています。これにより、検索結果を受けてviewControllerが次の処理(pushViewController:animated:)を行う事、が可能になります。

検索結果を表示する (EEEETwitterSearchResult〜)

表示するだけなので割愛します。

感想

  • MVVMによりViewControllerがシンプルになる (気がする)
    • ViewControllerを簡素化しようとしたら自然とMVVMになりそう (=あらかじめMVVMを意識すると組み立てやすい気がする)
  • ReactiveCocoaを使う事で状態管理がシンプルになる
    • バインディングに使う程度だと導入が楽そう
    • RACSignalを駆使しようとすると発想の転換が必要 (勉強不足。FRPを採用しようと思ったら駆使しないと厳しいだろうけど)
  • アニメーションに不利という意見があるけど、まだ未体験
    • 体験してみて何か書きます

ライブラリにどっぷり漬かるのは好きじゃないのですが、FRPの方針が好きなのでもう少し勉強してみようと思います。