0

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?

Matt Hupman
  • 195
  • 7

2 Answers2

1

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.

Dave Lee
  • 6,299
  • 1
  • 36
  • 36
  • Great call on the if:then:else usage - I'll update to that. Regarding the animateWithDuration: call, I wasn't able to get this code working without concat'ing the result of two animateWithDuration: calls and including an delay: operator on the second call. I think this is because animateWithDuration: immediately enqueues a UIView animation whenever it receives a value via subscribeNext:. Without the delay:, the first animation (fade out) seems to get lost. – Matt Hupman Mar 12 '14 at 16:04
  • Hmm. Might be best to ask on the RCL repo. The [docs for `-animateWithDuration:`](https://github.com/ReactiveCocoa/ReactiveCocoaLayout/blob/5f6bdf4a197073d4cc4fd2e30578650eea3d8684/ReactiveCocoaLayout/RACSignal%2BRCLAnimationAdditions.h#L113-L116) (and the code) indicate that concat/sequential ordering is the default. – Dave Lee Mar 12 '14 at 16:19
  • Yeah I'll open an issue there. For this example, I was able to remove the concat: (and use a single animateWithDuration:) by adding a delay: between the [RACSignal return]'s. This seems to more closely follow what the RCL docs suggest. Thanks! – Matt Hupman Mar 12 '14 at 16:31
  • I think I screwed up your suggested edit (which I've never used before). I'll edit to what I think you had, let me know if it's right. – Dave Lee Mar 12 '14 at 16:45
0

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];
}];
Anton Gaenko
  • 8,929
  • 6
  • 44
  • 39