今、一部で話題のReactive Programmingですが、Objective-CにもReactiveCocoaというフレームワークが存在します。ということでReactiveCocoaを試しに使ってみます。
GitHubにソースコードを置きました。
iOSアプリのシングルビュー構成のプロジェクトを作成します。
今回はReactiveCocoaはCocoaPodsでインストールします。Podfileを作成してpod ‘ReactiveCocoa’と記述します。
まず、テキストフィールドへの入力によってボタンの表示・非表示を行ってみます。
StoryboardでビューにUITextFieldを二つとUIButtonを一つ配置し、コントローラのヘッダにプロパティを作成します。
@property (weak, nonatomic) IBOutlet UITextField *usernameTextField; @property (weak, nonatomic) IBOutlet UITextField *passwordTextField; @property (weak, nonatomic) IBOutlet UIButton *logInButton; |
コントローラのviewDidLoadで以下のように記述します。
[self.usernameTextField.rac_textSignal subscribeNext:^(NSString *text) { self.logInButton.hidden = (text.length == 0); }]; |
UITextFieldのカテゴリメソッドrac_textSignalでtextの変更のシグナルのRACSignalが取得できます。RACSignalとはその名の通りイベントのシグナルです。
シグナルが正常に発生するとsubscribeNext:で登録したブロックが実行されます。ログインボタンのhiddenにテキスト長が0かの真偽値を設定しています。これでusernameのtextが入力されていない場合にlogInButtonが非表示になり、値を入力するとlogInButtonが表示されます。
今度はusernameとpasswordに入力するとログインボタンを表示するように変更してみます。先ほどのコードを削除して以下のコードを記述します。
[[RACSignal combineLatest:@[self.usernameTextField.rac_textSignal, self.passwordTextField.rac_textSignal] reduce:^(NSString *username, NSString *password) { return @(username.length == 0 || password.length == 0); }] subscribeNext:^(NSNumber *hidden) { self.logInButton.hidden = hidden.boolValue; }]; |
二つのUITextFieldのシグナルを[RACSignal combineLatest:reduce:]でまとめて、一つのシグナルとしてあつかいます。combineLatestで二つのシグナルを登録して、reduceで二つのテキストの長さのいずれかが0の場合にYES(のNSNumber)を返しています。
これでusernameとpasswordのtextが入力されていない場合にlogInButtonが非表示になり、どちらともに値を入力するとlogInButtonが表示されます。
次に非同期処理としてTwitterからユーザ名を取得してみます。
Storyboardにボタンとラベルを追加して、コントローラのヘッダのプロパティにバインドします。
@property (weak, nonatomic) IBOutlet UIButton *twitterButton; @property (weak, nonatomic) IBOutlet UILabel *twitterLabel; |
viewDidLoadには以下のように記述します。
@weakify(self); [[self.twitterButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *button) { [[[[TwitterManager sharedManager] requestAccessToAccounts] sequenceNext:^{ return [[TwitterManager sharedManager] fetchName]; }] subscribeNext:^(NSString *name) { @strongify(self); dispatch_async(dispatch_get_main_queue(), ^{ self.twitterLabel.text = name; }); } error:^(NSError *error) { @strongify(self); dispatch_async(dispatch_get_main_queue(), ^{ self.twitterLabel.text = error.localizedDescription; }); }]; }]; |
rac_signalForControlEvents:でボタンのイベントのシグナルを取得して、subscribeNext:でボタンのイベントで実行するブロックを登録しています。
TwitterManagerのrequestAccessToAccountsを実行すると、これもシグナルを返します。それが成功すると、fetchNameを実行して、さらにシグナルを返しています。そのシグナルが成功するとtwitterLabel.textに名前を設定し、エラーが発生するとエラー内容を設定しています。
TwitterManagerでの非同期処理の記述の仕方は以下のようになります。
+ (TwitterManager *)sharedManager { static TwitterManager *_instance = nil; @synchronized (self) { if (_instance == nil) { _instance = [[self alloc] init]; } } return _instance; } - (id)init { self = [super init]; if (self) { self.accountStore = [[ACAccountStore alloc] init]; self.accountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter]; } return self; } - (RACSignal *)requestAccessToAccounts { RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id <RACSubscriber> subscriber) { [self.accountStore requestAccessToAccountsWithType:self.accountType options:nil completion:^(BOOL granted, NSError *error) { if (granted) { NSArray *accounts = [self.accountStore accountsWithAccountType:self.accountType]; if (accounts.count > 0) { self.account = accounts[0]; [subscriber sendNext:nil]; } else { [subscriber sendError:[NSError errorWithDomain:@"ERRORDOMAIN" code:0 userInfo:nil]]; } } else { [subscriber sendError:(error ? error : [NSError errorWithDomain:@"ERRORDOMAIN" code:0 userInfo:nil])]; } [subscriber sendCompleted]; }]; return [RACDisposable disposableWithBlock:nil]; }]; return signal; } - (RACSignal *)fetchName { @weakify(self); RACSignal *signal = [RACSignal startWithScheduler:[RACScheduler scheduler] subjectBlock:^(RACSubject *subject) { @strongify(self); NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://api.twitter.com/1.1/users/show.json?screen_name=%@", self.account.username]]; SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodGET URL:url parameters:nil]; request.account = self.account; [request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) { if (error) { [subject sendError:error]; } else { NSError *subError; NSDictionary *status = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingMutableLeaves error:&subError]; if (subError) { [subject sendError:subError]; } else if (status[@"name"]) { [subject sendNext:status[@"name"]]; } else { [subject sendError:[NSError errorWithDomain:@"ERRORDOMAIN" code:2 userInfo:nil]]; } } [subject sendCompleted]; }]; }]; return signal; } |
requestAccessToAccountsは[RACSignal createSignal:]でシグナルを作成して返しています。リクエストが成功すると[subscriber sendNext:status[@”name”]]でシグナルを送り、エラーの場合はsendError:でエラーを送ります。完了するとsendCompletedを送ります。RACSignalではsubscribeNext:やsubscribeError:以外にもsubscribeCompletedで完了通知を受けとることができます。return [RACDisposable disposableWithBlock:];でキャンセルする時の処理を記述して返します。今回はキャンセル処理は無いのでnilを渡しています。(createSignal:って非同期処理に利用していいのかな?このあたり良く理解していないです。)
fetchNameでは[RACSignal startWithScheduler:subjectBlock:]でシグナルを作成して返しています。RACDisposableを返さない事以外はcreateSignal:とほぼ同様のようです。
ということで、ReactiveCocoaを試してみましたが、非Reactive Programmingとはかなり記述が変わってきますね。ReativeCocoaを使いこなせばUIイベントの処理や非同期処理が記述しやすくなる感触が得られました。