ReactiveCocoaでより良い世界を

ほとんどのアプリケーションは、イベントを待って処理するのに多くの時間を費やします。 ユーザーは、インターフェースとの対話を期待しています。 ネットワーク要求への応答を待っています。 非同期操作の完了を待機しています。 従属値の変更を待っています。 そして、その後にのみ反応します。



これらの期待と反応はすべて、さまざまな方法で実現できます。 それらについて話したり、高レベルの形式でバインドしたり作成したりすることは困難になります。 しかし、私たちはより良くすることができます。



そのため、GitHub for Macの背後にある魔法の一部であるReactiveCocoa(RAC)を公開することにしました。 RACは、 値のシーケンス構成および変換するためのフレームワークです





それは本当に何ですか?



具体化しましょう。 ReactiveCocoaは多くの興味深い機能を提供します:

  1. 将来のデータを操作する機能。
  2. 状態の数と可変性を減らす方法。
  3. プロパティ間の動作と関係を定義する宣言的な方法。
  4. 非同期操作用の統合された高レベルインターフェイス。
  5. 優れたKVOベースのAPI。


RACは、新しい価値を期待し、その変化に対応する状況向けに設計されていることを理解するまで、これは少し混乱しているように見えるかもしれません。



RACの真の美しさは、さまざまな一般的な状況に適応できることです。



十分な話。 それが実際にどのように見えるか見てみましょう。





RACは、KVOを使用して、KVO互換プロパティから一連の値を提供できます。 たとえば、 ユーザー名プロパティの変更を確認できます。

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





これはクールですが、KVOを介した優れたAPIにすぎません。 シーケンスを組み合わせて複雑な動作を表現すると、本当に興味深いことが起こります。



ユーザーが一意の名前を入力したかどうかを確認したいとしますが、最初の3回の試行からのみです。

 [[[[RACObserve(self, username) distinctUntilChanged] take:3] filter:^(NSString *newUsername) { return [newUsername isEqualToString:@"joshaber"]; }] subscribeNext:^(id _) { NSLog(@"Hi me!"); }];
      
      





ユーザー名の変更を確認し、小さな変更を除外して、最初の3つの値のみを取得し、新しい値がjoshaberの場合、特別な挨拶を表示します。



それで何?


RACなしでこれを達成するために何をしなければならないかを考えてください。 すなわち:



RACを使用すると、状態の数、テンプレートコードの数を減らす、コードのローカリゼーションを改善する、意図を明確に表現するなど、同じことが可能になります。



他に何?


シーケンスを組み合わせることができます。

 [[RACSignal combineLatest:@[RACObserve(self, password), RACObserve(self, passwordConfirmation)] reduce:^id(NSString *currentPassword, NSString *currentConfirmPassword) { return [NSNumber numberWithBool:[currentConfirmPassword isEqualToString:currentPassword]]; }] subscribeNext:^(NSNumber *passwordsMatch) { self.createEnabled = [passwordsMatch boolValue]; }];
      
      





passwordまたはpasswordConfirmationプロパティが変更されるたびに、最後の2つの値をマージし、それらをBOOLにキャストして、一致するかどうかを確認します。 次に、結果を使用して作成ボタンを有効または無効にします。



コミュニケーションズ


RACを使用して、条件および変換との強力な関係を取得できます。

 RAC(self, helpLabel.text) = [[RACObserve(self, help) filter:^(NSString *newHelp) { return newHelp != nil; }] map:^(NSString *newHelp) { return [newHelp uppercaseString]; }];
      
      





これにより、ヘルププロパティがnilでない場合、ヘルプラベルのテキストがヘルププロパティにバインドされ、その後、文字列が大文字に変換されます(ユーザーが叫んでいるときに好きになるため)。



非同期性


RACは非同期操作にも非常によく同意します。



たとえば、いくつかの並列操作が完了するとすぐにブロックを呼び出すことができます。

 [[RACSignal merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]] subscribeCompleted:^{ NSLog(@"They're both done!"); }];
      
      





または、非同期操作をバインドします。

 [[[[client loginUser] flattenMap:^(id _) { return [client loadCachedMessages]; }] flattenMap:^(id _) { return [client fetchMessages]; }] subscribeCompleted:^{ NSLog(@"Fetched all messages."); }];
      
      





これにより、承認され、キャッシュされたメッセージが受信され、サーバーからメッセージがダウンロードされ、「Fetched all messages。」と表示されます。



また、単に作業をバックグラウンドキューに転送することもできます。

 [[[[[client fetchUserWithUsername:@"joshaber"] deliverOn:[RACScheduler scheduler]] map:^(User *user) { // this is on a background queue return [[NSImage alloc] initWithContentsOfURL:user.avatarURL]; }] deliverOn:RACScheduler.mainThreadScheduler] subscribeNext:^(NSImage *image) { // now we're back on the main queue self.imageView.image = image; }];
      
      





または、潜在的な傍受状態に対処します。



たとえば、非同期呼び出しの結果でプロパティを更新できますが、完了前にプロパティが変更されていない場合のみです。

 [[[self loadDefaultMessageInBackground] takeUntil:[RACObserve(self.message) skip:1]] toProperty:@keypath(self.message) onObject:self];
      
      





どのように機能しますか?



RACは非常に単純です。 すべて信号で構成されています。



サブスクライバーはシグナルをサブスクライブします。 シグナルは、 nexterror 、およびcompletedイベントをサブスクライバーに送信します。 そして、これらがイベントを送信する単なるシグナルである場合、重要な質問は次のとおりです。



信号作成


シグナルは、いつどのイベントが送信されるかに関する動作を決定します。 + [RACSignal createSignal:]を使用して独自の信号を作成できます。

 RACSignal *helloWorld = [RACSignal createSignal:^(id<RACSubscriber> subscriber) { [subscriber sendNext:@"Hello, "]; [subscriber sendNext:@"world!"]; [subscriber sendCompleted]; return nil; }];
      
      





+ [RACSignal createSignal:]に渡すブロックは、信号が新しいサブスクライバーを受信するたびに呼び出されます。 新しいサブスクライバーがブロックに置き換えられるため、イベントを送信できます。 上記の例では、「Hello」、次に「world!」を送信してから終了する信号を作成しました。



入れ子信号


helloWorldシグナルに基づいて別のシグナルを作成することもできます。

 RACSignal *joiner = [RACSignal createSignal:^(id<RACSubscriber> subscriber) { NSMutableArray *strings = [NSMutableArray array]; return [helloWorld subscribeNext:^(NSString *x) { [strings addObject:x]; } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ [subscriber sendNext:[strings componentsJoinedByString:@""]]; [subscriber sendCompleted]; }]; }];
      
      





これでジョイナー信号があります。 誰かがジョイナーにサインアップすると、自動的にhelloWorldシグナルにサインアップします。 JoinerhelloWorldから受け取ったすべての値を追加し、 helloWorldが完了すると、受け取ったすべての行を1つの行に結合して送信し、終了します。



このようにして、互いに基づいて信号を作成し、複雑な動作を表現できます。



RACには、まさにそれを行うRACSignalの一連の操作が含まれています。 元の信号を受け入れ、特定の動作を伴う新しい信号を返します。



追加情報



ReactiveCocoaはMacとiOSの両方で動作します。 詳細については、README、およびReactiveCocoaプロジェクトにあるドキュメントをお読みください。



.NET開発者にとって、これはすべて非常に馴染みのあるように思えるかもしれません。 実際、ReactiveCocoaは.NET用のReactive Extensions(Rx)のObjective-Cバージョンです。



多くのRx原則がRACに適用されます。 Rxについての非常に優れた情報源は次のとおりです。

Reactive Extensions MSDNエントリ

.NETのリアクティブ拡張機能

Rx-チャンネル9ビデオ

Reactive Extensions wiki

101 Rxサンプル

Reactive ExtensionsとLINQのプログラミング



翻訳は、元の記事の著者とこのフレームワークの同意を得て、Josh Abernathyが発行しています。



All Articles