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

UIAlertViewとUIActionSheetをblocksで使いやすくする

UIAlertViewやUIActionSheetはdelegateで選択結果を取得して処理を分岐します。このdelegateでというのが結構面倒ですが、iOS4からはblocksが利用できるので、blocksを利用すればUIAlertViewをつくると同時にボタンに対応する動作も記述できて使いやすくなります。

ネット上にはblocksでの拡張情報がいくつかありますが、以下がネットの情報を元に私が作成したblocks版UIAlertViewとUIActionSheetです。

CTAlertView.h

#import <UIKit/UIKit.h>
 
@class CTAlertView;
 
typedef void (^CTAlertViewButtonCallback)(CTAlertView *alertView, NSInteger index);
 
@interface CTAlertView : UIAlertView <UIAlertViewDelegate> {
@private
    void (^willPresentCallback_)(CTAlertView *);
    void (^didPresentCallback_)(CTAlertView *);
    void (^willDismissCallback_)(CTAlertView *, NSInteger);
    void (^didDismissCallback_)(CTAlertView *, NSInteger);
 
    NSMutableArray *buttonCallbacks_;
}
 
- (id)initWithTitle:(NSString *)title
            message:(NSString *)message
  cancelButtonTitle:(NSString *)cancelButtonTitle;
 
-(void)addButtonWithTitle:(NSString*)title callback:(CTAlertViewButtonCallback)callback;
-(void)setCancelButtonCallback:(CTAlertViewButtonCallback)block;
-(void)setWillPresentCallback:(void (^)(CTAlertView *alertView))block;
-(void)setDidPresentCallback:(void (^)(CTAlertView *alertView))block;
-(void)setWillDismissCallback:(void (^)(CTAlertView *alertView, NSInteger index))block;
-(void)setDidDismissCallback:(void (^)(CTAlertView *alertView, NSInteger index))block;
 
@end

CTAlertView.h

#import "CTAlertView.h"
 
@interface CTAlertView ()
 
@property (nonatomic, retain) NSMutableArray *buttonCallbacks;
 
- (BOOL)hasCancelButton;
 
@end
 
@implementation CTAlertView
 
@synthesize buttonCallbacks=buttonCallbacks_;
 
- (id)initWithTitle:(NSString *)title
            message:(NSString *)message
  cancelButtonTitle:(NSString *)cancelButtonTitle
{
    self = [super initWithTitle:title message:message delegate:nil cancelButtonTitle:cancelButtonTitle otherButtonTitles:nil];
    if (self) {
        self.delegate = self;
        self.buttonCallbacks = [NSMutableArray array];
        if ([self hasCancelButton]) {
            [self setCancelButtonCallback:^(CTAlertView *alertView, NSInteger index){}];
        }
    }
    return self;
}
 
- (void)dealloc
{
    [willPresentCallback_ release];
    [didPresentCallback_ release];
    [willDismissCallback_ release];
    [didDismissCallback_ release];
    self.buttonCallbacks = nil;
 
    [super dealloc];
}
 
-(void)addButtonWithTitle:(NSString*)title callback:(CTAlertViewButtonCallback)callback
{
    id obj = [NSNull null];
    if (callback) obj = [[callback copy] autorelease];
 
    [self addButtonWithTitle:title];
    [self.buttonCallbacks addObject:obj];
}
 
-(void)setCancelButtonCallback:(CTAlertViewButtonCallback)callback
{
    if (![self hasCancelButton]) return;
 
    if ([self.buttonCallbacks count] &gt; 0) {
        [self.buttonCallbacks removeObjectAtIndex:0];
    }
 
    [self.buttonCallbacks insertObject:[[callback copy] autorelease] atIndex:0];
}
 
-(void)setWillPresentCallback:(void (^)(CTAlertView *alertView))block
{
    [willPresentCallback_ release];
    willPresentCallback_ = [block retain];
}
 
-(void)setDidPresentCallback:(void (^)(CTAlertView *alertView))block
{
    [didPresentCallback_ release];
    didPresentCallback_ = [block retain];
}
 
-(void)setWillDismissCallback:(void (^)(CTAlertView *alertView, NSInteger index))block
{
    [willDismissCallback_ release];
    willDismissCallback_ = [block retain];
}
 
-(void)setDidDismissCallback:(void (^)(CTAlertView *alertView, NSInteger index))block
{
    [didDismissCallback_ release];
    didDismissCallback_ = [block retain];
}
 
- (BOOL)hasCancelButton
{
    return (self.cancelButtonIndex == 0);
}
 
#pragma mark -
#pragma mark UIAlertViewDelegate
 
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    id callback = [self.buttonCallbacks objectAtIndex:buttonIndex];
    if (![callback isMemberOfClass:[NSNull class]]) {
        ((CTAlertViewButtonCallback)callback)((CTAlertView *)alertView, buttonIndex);
    }
}
 
-(void)willPresentAlertView:(UIAlertView *)alertView
{
    if(willPresentCallback_) willPresentCallback_((CTAlertView *)alertView);
}
 
-(void)didPresentAlertView:(UIAlertView *)alertView
{
    if(didPresentCallback_) didPresentCallback_((CTAlertView *)alertView);
}
 
-(void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex
{
    if(willDismissCallback_) willDismissCallback_((CTAlertView *)alertView, buttonIndex);
}
 
-(void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
    if(didDismissCallback_) didDismissCallback_((CTAlertView *)alertView, buttonIndex);
}
 
@end

CTActionSheet.h

#import <UIKit/UIKit.h>
 
@class CTActionSheet;
 
typedef void (^CTActionSheetButtonCallback)(CTActionSheet *actionSheet, NSInteger index);
 
@interface CTActionSheet : UIActionSheet <UIActionSheetDelegate> {
@private
    NSMutableArray *buttonCallbacks_;
    void (^willPresentCallback_)(CTActionSheet *);
    void (^didPresentCallback_)(CTActionSheet *);
    void (^willDismissCallback_)(CTActionSheet *, NSInteger);
    void (^didDismissCallback_)(CTActionSheet *, NSInteger);
}
 
- (id)initWithTitle:(NSString *)title;
 
- (void)addCancelButtonWithTitle:(NSString *)title callback:(CTActionSheetButtonCallback)callback;
- (void)addDestructiveButtonWithTitle:(NSString *)title callback:(CTActionSheetButtonCallback)callback;
- (void)addButtonWithTitle:(NSString *)title callback:(CTActionSheetButtonCallback)callback;
- (void)setWillPresentCallback:(void (^)(CTActionSheet *actionSheet))callback;
- (void)setDidPresentCallback:(void (^)(CTActionSheet *actionSheet))callback;
- (void)setWillDismissCallback:(void (^)(CTActionSheet *actionSheet, NSInteger index))callback;
- (void)setDidDismissCallback:(void (^)(CTActionSheet *actionSheet, NSInteger index))callback;
 
@end

CTActionSheet.m

#import "CTActionSheet.h"
 
@interface CTActionSheet ()
 
@property (nonatomic, retain) NSMutableArray *buttonCallbacks;
 
@end
 
@implementation CTActionSheet
 
@synthesize buttonCallbacks=buttonCallbacks_;
 
- (id)initWithTitle:(NSString *)title
{
    self = [super initWithTitle:title delegate:nil cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
    if (self) {
        self.delegate = self;
        self.buttonCallbacks = [NSMutableArray array];
    }
    return self;
}
 
- (void)dealloc
{
    self.buttonCallbacks = nil;
    [willPresentCallback_ release];
    [didPresentCallback_ release];
    [willDismissCallback_ release];
    [didDismissCallback_ release];
 
    [super dealloc];
}
 
- (void)addCancelButtonWithTitle:(NSString *)title callback:(CTActionSheetButtonCallback)callback
{
    NSUInteger index = [self.buttonCallbacks count];
    [self addButtonWithTitle:title callback:callback];
    self.cancelButtonIndex = index;
}
 
- (void)addDestructiveButtonWithTitle:(NSString *)title callback:(CTActionSheetButtonCallback)callback
{
    NSUInteger index = [self.buttonCallbacks count];
    [self addButtonWithTitle:title callback:callback];
    self.destructiveButtonIndex = index;
}
 
- (void)addButtonWithTitle:(NSString*)title callback:(CTActionSheetButtonCallback)callback
{
    id obj = [NSNull null];
    if (callback) obj = [[callback copy] autorelease];
 
    [self addButtonWithTitle:title];
    [self.buttonCallbacks addObject:obj];
}
 
-(void)setWillPresentCallback:(void (^)(CTActionSheet *actionSheet))callback
{
    [willPresentCallback_ release];
    willPresentCallback_ = [callback retain];
}
 
-(void)setDidPresentCallback:(void (^)(CTActionSheet *actionSheet))callback
{
    [didPresentCallback_ release];
    didPresentCallback_ = [callback retain];
}
 
-(void)setWillDismissCallback:(void (^)(CTActionSheet *actionSheet, NSInteger index))callback
{
    [willDismissCallback_ release];
    willDismissCallback_ = [callback retain];
}
 
-(void)setDidDismissCallback:(void (^)(CTActionSheet *actionSheet, NSInteger index))callback
{
    [didDismissCallback_ release];
    didDismissCallback_ = [callback retain];
}
 
#pragma mark -
#pragma mark UIActionSheetDelegate
 
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    id callback = [self.buttonCallbacks objectAtIndex:buttonIndex];
    if (![callback isMemberOfClass:[NSNull class]]) {
        ((CTActionSheetButtonCallback)callback)((CTActionSheet *)actionSheet, buttonIndex);
    }
}
 
- (void)willPresentActionSheet:(UIActionSheet *)actionSheet
{
    if (willPresentCallback_) willPresentCallback_((CTActionSheet *)actionSheet);
}
 
- (void)didPresentActionSheet:(UIActionSheet *)actionSheet
{
    if (didPresentCallback_) didPresentCallback_((CTActionSheet *)actionSheet);
}
 
- (void)actionSheet:(UIActionSheet *)actionSheet willDismissWithButtonIndex:(NSInteger)buttonIndex
{
    if (willDismissCallback_) willDismissCallback_((CTActionSheet *)actionSheet, buttonIndex);
}
 
- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex
{
    if (didDismissCallback_) didDismissCallback_((CTActionSheet *)actionSheet, buttonIndex);
}
 
@end

使い方は

CTAlertView *alert = [[[CTAlertView alloc] initWithTitle:@"title" message:@"message" cancelButtonTitle:@"Cancel"] autorelease];
[alert setCancelButtonCallback:^(CTAlertView *alertView, NSInteger index) {
    // Cancel action
}];
[alert addButtonWithTitle:@"Foo" callback:^(CTAlertView *alertView, NSInteger index) {
    // Foo action
}];
[alert addButtonWithTitle:@"Bar" callback:^(CTAlertView *alertView, NSInteger index) {
    // Bar action
}];
[alert show];

のようにします。キャンセルタイトルは指定しなければキャンセルボタンは作成されません。また、キャンセルボタンコールバックを指定しなくても動作します。

CTActionSheetではキャンセルボタンの指定を- (void)addCancelButtonWithTitle:(NSString *)title callback:(CTActionSheetButtonCallback)callbackで行います。

また、テストは不十分なので、バグがあればコメントよろしくです。

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

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

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

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

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

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