逆引きReactiveCocoa: シグナルにイベント発生時に対する処理を挟み込む

ReactiveCocoaのドキュメントにもあるように、-do...系のメソッドを使用すると、シグナルを購読せずに副作用を挟み込むことができます。

RACSignal *newSignal = [[[signal 
    doNext:^(id x) {
        NSLog(@"next");
    }] 
    doError:^(NSError *error) {
        NSLog(@"error");
    }] 
    doCompleted:^{
        NSLog(@"completed");
    }];

シグナルを購読する終端であれば、各種処理を-subscribe...:系のメソッドのブロック内で行うこともできますが、シグナルに手を加えてシグナルを返すようなメソッドでは便利に使用することができます。

また、例えば-doNext:を重ねて記述した場合、記述順(lexical order)に処理が実行されます。

[[[signal
    doNext:^(id x) {
        NSLog(@"1st");
    }]
    doNext:^(id x) {
        NSLog(@"2nd");
    }]
    subscribeNext:^(id x) {
        NSLog(@"-subscribeNext:");
    }];
// 1st
// 2nd
// -subscribeNext:

シグナルの終了時の処理として正常系/異常系のどちらでも同じ処理を行いたい場合、-finally:が使用できます。

// 以下の2つは同義。

[signal finally:^{
    [self doSomeStuff];
}];

[[signal
    doError:^(NSError *error) {
        [self doSomeStuff];
    }]
    doCompleted:^{
        [self doSomeStuff];
    }];

補足

ReactiveCocoa 3.0では-finally:は非推奨となり、-doFinished:が追加(としてリネーム)されています。

逆引きReactiveCocoa: 毎時0分に処理を行う

元ネタ: ios - Perform action on the hour, every hour, with ReactiveCocoa - Stack Overflow


現在時刻から1時間毎だと毎時0分とならないので、まずは次の時までの分を得ておきます。

NSDateComponents *components = [[[NSCalendar sharedCalendar] calendar] components:NSMinuteCalendarUnit fromDate:[NSDate date]];
NSInteger minutesToNextHour = 60 - components.minute;

いったん現在時刻から次の時までのタイマーシグナルを生成し、1回限りで終了させた後に改めて60分間隔のタイマーシグナルを繋げて完成です。

RACSignal *updateEventSignal = [[[RACSignal
    interval:(60 * minutesToNextHour)]
    take:1]
    concat:[RACSignal interval:3600]];

逆引きReactiveCocoa: @YES/@NOをトグルで送るRACCommandを用意する

元ネタ: Command which sends alternating YES/NO values · Issue #767 · ReactiveCocoa/ReactiveCocoa


ボタン・ジェスチャーなどのUI操作とRACSignalを仲介するクラスとしてRACCommandがあります。これを用いて、状態(フラグとなるインスタンス変数/プロパティ)を持たずにトグルボタンを表現するパターンです。

// ブロックの戻り値が`[command execute:senderOrValue]`の戻り値となる。
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
    return [RACSignal return:RACUnit.defaultUnit];
}];

RACSignal *alternating = [[[command.executionSignals // `-execute:`の度にその戻り値のシグナルを送るシグナル。
    flatten]
    startWith:RACUnit.defaultUnit] // ユーザー操作前に初期値がセットされるようにする。
    scanWithStart:@NO combine:^(NSNumber *previous, id _) {
        // 前回の値を反転したブール値を返し、
        // コマンドの実行によって得られた値は無視する(_ == RACUnit.defaultUnit)。
        //
        // 最初に`@NO`を反転するため、シグナルの初回の値は`@YES`となる。
        return @(!previous.boolValue);
    }];
RAC(self, onOff) = alternating;

// `UIControlEventTouchUpInside`のイベントで`-execute:`される。
button.rac_command = command;

逆引きReactiveCocoa: シグナルの現在の値だけでなく、直前の値にアクセスする

元ネタ: How to obtain KVO old value using ReactiveCocoa 2.0 API? · Issue #762 · ReactiveCocoa/ReactiveCocoa


KVOのオプションを使用する場合

ReactiveCocoaではRACObserve()マクロによってKVOを非常に簡単に扱うことができますが、それだけでは、通常のKVOで使用するaddObserver:forKeyPath:options:context:の場合には指定できるNSKeyValueObservingOptions (NSKeyValueObservingOptionOldなど) を指定できません。

KVOのオプション指定を行いたい場合、-[NSObject rac_valuesAndChangesForKeyPath:options:observer:]を使用するとKVOのオプション指定を反映したシグナルを得ることができます。ただし、この戻り値のシグナルはRACObserve()とは違い、nextとして、変更後の値とチェンジディクショナリーからなるRACTupleを送るため、-reduceEach:を使用するなどしてタプルの値を分解・チェンジディクショナリーから変更前の値を取得するなどの必要があります。

[[self 
    rac_valuesAndChangesForKeyPath:keyPath options:NSKeyValueObservingOptionOld observer:nil] 
    reduceEach:^(id value, NSDictionary *changeDictionary) {
        id oldValue = changeDictionary[NSKeyValueChangeOldKey];
        return // 何かしらの値;
    }];

オペレーターを使用する場合

-[RACSignal combinePreviousWithStart:reduce:]を使用すると、reduce:のブロック引数で以下のように直前の値を使用することができます。

RACSignal *signal = [@[ @1, @2, @3, @4 ].rac_sequence signal];
[[signal 
    // 最初の`next`では`previous`に1つ目の引数(この場合は`@0`)が渡され、
    // 2回目以降の`next` では前回の`current`が渡される。
    combinePreviousWithStart:@0 reduce:^(NSNumber *previous, NSNumber *current) {
        return @(previous.integerValue + current.integerValue);
    }]
    subscribeNext:^(NSNumber *value) {
        NSLog(@"%d", value.integerValue);
    }];
// 1 (<= 0 + 1)
// 3 (<= 1 + 2)
// 5 (<= 2 + 3)
// 7 (<= 3 + 4)

逆引きReactiveCocoa: 副作用だけのシグナルをリフティング/バインディングに使用する

元ネタ: Best practice for signals that capture completed/error operations · Issue #653 · ReactiveCocoa/ReactiveCocoa


ログインやデータの永続化など、特に next として送る値がないシグナルの結果を -[NSObject rac_liftSelector:withSignals:]RAC() マクロでのバインディングに使用したい場合にどうするべきかというパターンです。

RACSignal *login = [[[self 
    // ログイン成功時には `completed`, 失敗時には `error` を送る。
    loginWithUserName:userName password:password] 
    concat:[RACSignal return:@YES]]
    catchTo:[RACSignal return:@NO]];

RAC(self, loggedIn) = login;
// or
[self rac_liftSelector:@selector(loginDidSuccess:) withSignals:login, nil];

とすると、-loginWithUserName:password: の実装側では、next として @YES/@NORACUnit.defaultUnit を送る必要がなくなります。

補足

元ネタでは [RACSignal return:@YES]; を返す部分に -then: を使用していましたが、そう遠くない内にリリースされるであろう ReactiveCocoa 3.0 では -then:非推奨 (deprecated) になりそうなので置き換えています。

ReactiveCocoaでタイマー処理 その2

こんなことがあり、

前回の追記として、一回処理を即時実行してから、インターバルで処理を行いたい場合は以下のように書けるかと思います。

// NSTimer
- (void)doAndThenRepeatByTimer {
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                                      target:self
                                                    selector:@selector(someMethod:)
                                                    userInfo:nil
                                                     repeats:YES];
    [timer fire];
}

// RAC
- (void)doAndThenRepeatBySignal {
    [[[RACSignal interval:5.0] startWith:[NSDate date]] subscribeNext:^(NSDate *date) {
        NSLog(@"%@", date);
    }];
}

-[RACSignal startWith:]を使用すると、引数で渡した値1つだけのシグナルが用意され、その値の後にレシーバーのシグナルの値が続いて発行される形になります。なので、+[RACSignal interval:]の場合と同じ型の値として、[NSDate date]で現在日時の値が即座に発行されるようにしています(interval:で発行されるのはその時々の[NSDate date])。

ReactiveCocoaでタイマー処理

最近一部で話題(と勝手に思っている)の ReactiveCocoa

NSTimerでも問題ないことですが、場合によっては別にメソッド作ったり、セレクター指定が面倒なこともあるので、以下のような簡単なタイマー処理をReactiveCocoaで書いてみます。

- (void)setUpTimer {
    [NSTimer scheduledTimerWithTimeInterval:5.0
                                     target:self
                                   selector:@selector(someMethod:)
                                   userInfo:nil
                                    repeats:YES];
}

- (void)someMethod:(NSTimer *)timer {
    NSLog(@"%@", timer);
}

これが次のようになります。

- (void)setUpTimer {
    [[RACSignal interval:5.0] subscribeNext:^(NSDate *date) {
        NSLog(@"%@", date);
    }];
}

簡単ですね!

キャンセルしたい場合は、-subscribeNextの戻り値のRACDisposableを-disposeすることは推奨されていないようなので、

@property (nonatomic, strong) RACSubject *cancelSubject;

- (void)setUpTimer {
    [[[RACSignal interval:5.0] takeUntil:self.cancelSubject] subscribeNext:^(NSDate *date) {
        NSLog(@"%@", date);
    }];
}

- (void)cancelTimer {
    [self.cancelSubject sendNext:[RACUnit defaultUnit]];
}

こんな感じにすれば良さそうです。takeUntil:の引数に指定したRACSignalが値を発行する(sendNext:)か完了する(sendCompleted)と、レシーバーのsignalが終了するようになります。

ちなみに +[RACSignal interval:] メソッドは内部で dispatch_source を使用していますねー。