Objective-CのFunctional Reactive ProgrammingフレームワークReactiveCocoaを試してみる

今、一部で話題の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イベントの処理や非同期処理が記述しやすくなる感触が得られました。

スレッド毎にNSManagedObjectContextを管理する

基本的にはNSManagedObjectContextはスレッド毎に作成して利用しなければいけません。以下は、その為にスレッド毎にContextを作成して管理するためのコードです。複雑なアプリでなければ、この管理法で十分でしょう。シングルトンなManagerクラスでコンテキストを管理する方法は簡単ですが、スレッドを多用する場合はスレッド毎に管理する方が良いでしょう。

managedObjectContextForCurrentThreadでコンテキストを取得し、保存は[NSManagedObjectContext save:&error]のように、クラスメソッドを呼びます。保存するとNSNotificationでコンテキスト自身のmanagedObjectContextDidSave:が実行され、メインスレッドのコンテキストに変更が反映されます。スレッド毎に自動的にthreadDictionaryにNSManagedObjectContextを登録し、スレッドが破棄されるとコンテキストも破棄されるようになっているので、明示的にコンテキストを破棄する必要はありません。

@interface NSManagedObjectContext (Extras)
+ (NSManagedObjectContext *)managedObjectContextForThread:(NSThread *)thread;
+ (NSManagedObjectContext *)managedObjectContextForCurrentThread;
+ (NSManagedObjectContext *)managedObjectContextForMainThread;
+ (BOOL)save:(NSError **)error;
@end
#import "NSManagedObjectContextExtras.h"
 
NSString * const NSManagedObjectContextThreadKey = @"NSManagedObjectContextThreadKey";
 
@interface NSManagedObjectContext ()
- (void)managedObjectContextDidSave:(NSNotification*)notification;
@end
 
@implementation NSManagedObjectContext (Extras)
 
+ (NSManagedObjectContext *)managedObjectContextForThread:(NSThread *)thread {
    NSMutableDictionary *threadDictionary = [thread threadDictionary];
    NSManagedObjectContext *context = [threadDictionary objectForKey:NSManagedObjectContextThreadKey];
 
    if (!context) {
#ifdef TARGET_OS_IPHONE
        id appDelegate = [[UIApplication sharedApplication] delegate];
#else
        id appDelegate = [NSApp delegate];
#endif
        NSManagedObjectContext *mainContext = [appDelegate managedObjectContext];
 
        if ([[NSThread currentThread] isMainThread]) {
            context = mainContext;
        } else {
            context = [[[NSManagedObjectContext alloc] init] autorelease];
            [context setPersistentStoreCoordinator:[mainContext persistentStoreCoordinator]];
        }
 
        [threadDictionary setObject:context forKey:NSManagedObjectContextThreadKey];
    }
 
    return context;
}
 
+ (NSManagedObjectContext *)managedObjectContextForCurrentThread {
    return [NSManagedObjectContext managedObjectContextForThread:[NSThread currentThread]];
}
 
+ (NSManagedObjectContext *)managedObjectContextForMainThread {
    return [NSManagedObjectContext managedObjectContextForThread:[NSThread mainThread]];
}
 
+ (BOOL)save:(NSError **)error {
    NSManagedObjectContext *context = [NSManagedObjectContext managedObjectContextForCurrentThread];
    BOOL isMainThread = [[NSThread currentThread] isMainThread];
 
    if (!isMainThread) {
        [[NSNotificationCenter defaultCenter] addObserver:context
                                                 selector:@selector(managedObjectContextDidSave:)
                                                     name:NSManagedObjectContextDidSaveNotification
                                                   object:context];
    }
 
    BOOL result = [context save:error];
 
    if (!isMainThread) {
        [[NSNotificationCenter defaultCenter] removeObserver:context
                                                        name:NSManagedObjectContextDidSaveNotification 
                                                      object:context];
    }
 
    return result;
}
 
- (void)managedObjectContextDidSave:(NSNotification*)notification {
    NSManagedObjectContext *context = [NSManagedObjectContext managedObjectContextForMainThread];
 
    [context performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:)
                              withObject:notification
                           waitUntilDone:YES];
}
@end

Objective-C/Cocoaにおける例外の利用についてのメモ

Objective-C/Cocoaでの例外の利用についてのメモ。

例外は以下のような予期されない論理上のエラーを伝播するために利用する。

  • 配列の添字が境界外である
  • 不変オブジェクトに変更を試みた
  • 引数の型が不正

このような例外はテストで捕捉して問題を解決するもので、アプリケーション利用時には例外を処理する必要は無いというのがあるべき姿である。

予期されるエラーを捕捉するために例外の代わりにエラー(NSError)を利用する。

オープンソースの統合ソフトウェア開発環境『Eclipse』がCocoaをサポート!: Podcast journal – ポッドキャストジャーナル

iPhoneアプリケーションの作成に使用されているCocoa。今まではCocoaと言うと統合開発環境としてXcodeを使っていた人が多いと思うのですが、選択肢として新たに「Eclipse」が加わりました。オープンソースの統合ソフトウェア開発環境『Eclipse』がCocoaをサポート!: Podcast journal – ポッドキャストジャーナル

Xcodeの方がIBとの親和性も高いだろうし、速度も有利だと思いますが、Eclipseの拡張性や他の言語プロジェクトとの連携を考えると、Eclipseも良さそうな気がします。

もっともXcodeが正式にPlugin対応すれば嬉しいんですが。

で、Eclipse 3.5をインストールしてみましたが、UI、特にメニューとPreferenceがMac的になってるんで、嬉しかったです。速度はまだインストールしただけなんでわかんないですが。