Interpolate animations and bindings

Note: I'm using ReactiveCocoaLayout for signal-based animations.

I have a UILabel that I'd like to bind to a NSString* property on a view model.

RACSignal* statusSignal = [RACObserve(self, viewModel.status) distinctUntilChanged];

Simple enough. However, now I'd like to add some fancy animations. What I want to happen, serially, when the status changes:

  1. Fade the label out (alpha from 1 -> 0)
  2. Apply the new text to the UILabel
  3. Fade the label in (alpha from 1 -> 0)

This is what I've been able to come up with so far:

RACSignal* statusSignal = [RACObserve(self, viewModel.status) distinctUntilChanged];

// An animation signal that initially moves from (current) -> 1 and moves from (current) -> 0 -> 1 after that
RACSignal* alphaValues = [[statusSignal flattenMap:^RACStream *(id _) {

    // An animation signal that moves towards a value of 1
    return [[[RACSignal return:@1]
             delay:animationDuration]
            animateWithDuration:animationDuration];

}] takeUntilReplacement:[[statusSignal skip:1] flattenMap:^RACStream *(id _) {

    // An animation signal that moves towards a value of 0, waits for that to complete, then moves towards a value of 1
    return [[[RACSignal return:@(0)]
             animateWithDuration:animationDuration]
            concat:[[[RACSignal return:@1]
                     delay:animationDuration]
                    animateWithDuration:animationDuration]];
}]];

RAC(self, statusLabel.alpha) = alphaValues;

// The initial status should be applied immediately.  Combined with the initial animation logic above, this will nicely fade in the first
// status.  Subsequent status changes are delayed by [animationDuration] in order to allow the "fade" animation (alpha from 1 -> 0) to
// finish before the text is changed.
RAC(self, statusLabel.text) = [[statusSignal take:1]
                               concat:[[statusSignal delay:animationDuration]
                                       deliverOn:[RACScheduler mainThreadScheduler]]];

This works, but I can't shake the feeling that it's a little... engineered. Much of the complication comes from my base case - The initial text should just fade in, subsequent text changes should fade out then fade in.

Any thoughts on how to simplify or optimize?

Answers


I haven't used RCL yet, so let me know if I've used -animateWithDuration: incorrectly.

First, I've defined a signal that sends @YES instead of the first status, and @NO instead of all following statuses. This is done by taking advantage of the -bind: operator which allows for custom per-subscriber variables via block capture.

RACSignal *isFirstStatus = [statusSignal bind:^{
    __block BOOL first = YES;
    return ^(id _, BOOL *stop) {
        BOOL isFirst = first;
        first = NO;
        return [RACSignal return:@(isFirst)];
    };
}];

Next, put isFirstStatus together with +if:then:else: to start with the initial animation signal, and then switch to the permanent animation signal.

RAC(self, statusLabel.alpha) = [[RACSignal
    if:isFirstStatus
        // Initially, an animation signal to 1.
        then:[RACSignal return:@1]
        // Subsequently, an animation signal to 0, then, to 1.
        else:[[[RACSignal return:@1] delay:animationDuration] startWith:@0]]
    animateWithDuration:animationDuration];

I've looked for a way to handle the text property update in-band with the animation via its completion, but didn't find anything I liked. This might be an opportunity to add an operator to RCL for this type of scenario, if there's not already a good way of doing that.


Just implement solution in a similar way.

[RACObserve(self, curChannel) subscribeNext:^(NSNumber* v) {
    BGChannel* ch = self.model.subscriptions[v.intValue];
    [UIView transitionWithView:self.channelLabel duration:0.75 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
        self.channelLabel.text = ch.name;
    } completion:nil];
}];

Need Your Help