Skip to content

Instantly share code, notes, and snippets.

@andrewsardone andrewsardone/imperative.m
Last active Aug 29, 2015

Embed
What would you like to do?
Animating UILabel
static void *kContext = &kContext;
- (void)viewDidLoad
{
[super viewDidLoad];
[self.viewModel addObserver:self
forKeyPath:@"instructionText"
options:NSKeyValueObservingOptionInitial
context:kContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == kContext) {
if ([keyPath isEqualToString:@"instructionText"]) {
[self updateFormTitleText:change[NSKeyValueChangeNewKey]
animated:(change[NSKeyValueChangeOldKey] != nil)];
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)updateFormTitleText:(NSString *)text animated:(BOOL)animated
{
if (animated) {
[UIView
animateWithDuration:0.2
animations:^{
self.formTitleLabel.alpha = 0.0;
}
completion:^(BOOL finished) {
self.formTitleLabel.text = text;
[UIView animateWithDuration:0.2 animations:^{
self.formTitleLabel.alpha = 1.0;
}];
}];
}
else self.formTitleLabel.text = text;
}

I want a UILabel to fade out and in when changing its text. This animation should not be applied on initial setting to prevent a flicker when the view first appears. It's possible that my Reactive Cocoa solution is preferrable to its “traditional” counterpart, but they really both suck.

I can't get away from this good piece of advice from David Rönnqvist:

If the animation mysteriously went away, the model value should be the expected end state.

I'm somewhat misapplying Rönnqvist's statement – the text property of a UILabel is orthogonal to the concerns of a backing model or presentation CALayer – but I think it's an applicable goal. I have two tasks and I want them decoupled:

  • The UILabel needs to have its text changed over time. This is a binding that I should declare, RAC(self.formTitleLabel, text) = RACObserver(self.viewModel, instructionText).
  • I want this change to be animated, but that should be tangential – the text needs to change, the animation is just for style. There's also a small amount of logic behind this style – only applying the animation after the first change.

In both examples, the text is updated in if-else branches, one of which is buried within some completion block. In the RAC example, we're getting its nicer KVO-like API, but we're not getting the declarative code that better expresses our intent. To add insult to injury, there's this empty -subscribeNext: that's quite gross.

Clearly, there's room for improvement.

I guess I could grab the -updateFormTitleText:animated: method from the imperative example and lift it into our reactive world:

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self rac_liftSelector:@selector(updateFormTitleText:animated:)
          withSignalsFromArray:@[
              [RACObserve(self.viewModel, instructionText) take:1],
              [RACSignal return:@NO],
          ]];

    [self rac_liftSelector:@selector(updateFormTitleText:animated:)
          withSignalsFromArray:@[
              [RACObserve(self.viewModel, instructionText) skip:1],
              [RACSignal return:@YES],
          ]];
}

- (void)updateFormTitleText:(NSString *)text animated:(BOOL)animated
{
    if (animated) {
        [UIView
            animateWithDuration:0.2
            animations:^{
                self.formTitleLabel.alpha = 0.0;
            }
            completion:^(BOOL finished) {
                self.formTitleLabel.text = text;
                [UIView animateWithDuration:0.2 animations:^{
                    self.formTitleLabel.alpha = 1.0;
                }];
            }];
    }
    else self.formTitleLabel.text = text;
}

I think the intent is still too obfuscated, and there doesn't appear to be a way to avoid intermingling the property update and the animations. An improvement, I suppose.

- (void)viewDidLoad
{
[super viewDidLoad];
// This is a hack to achieve the effect of a UILabel that fades out and back
// in when changing its text. The animation does not occur when the view first
// appears. Why does this suck?
//
// - A `-subscribeNext:` that does nothing but give us a subscription
// - The @weakify/@strongify song-and-dance due to the side-effect nature
// of tinkering with multiple states of the UILabel
@weakify(self);
[[RACObserve(self.viewModel, instructionText)
scanWithStart:@0
reduce:^id(NSNumber *countOfChanges, NSString *text) {
@strongify(self);
if (countOfChanges.integerValue > 0) {
[UIView
animateWithDuration:0.2
animations:^{
self.formTitleLabel.alpha = 0.0;
}
completion:^(BOOL finished) {
self.formTitleLabel.text = text;
[UIView animateWithDuration:0.2 animations:^{
self.formTitleLabel.alpha = 1.0;
}];
}];
}
else self.formTitleLabel.text = text;
return @(countOfChanges.integerValue + 1);
}] subscribeNext:^(id _){}];
}
@PaulTaykalo

This comment has been minimized.

Copy link

PaulTaykalo commented Feb 24, 2014

Maybe something like this?

    CGFloat animationDuration = 0.3;
    RACSignal * instructionText = RACObserve(self.viewModel, instructionText);
    RACSignal * firstTime = [instructionText take:1];
    RACSignal * otherTimes = [[instructionText skip:1] throttle:animationDuration];

    [firstTime doNext:^(id x) {
        // Without animation
        [self updateFormTitleText:x animated:NO];

    }];

    [otherTimes doNext:^(id x) {
        [self updateFormTitleText:x animated:YES];
    }];
@PaulTaykalo

This comment has been minimized.

Copy link

PaulTaykalo commented Feb 25, 2014

Or even like this

    __weak id weakSelf = self;
    [[[RACObserve(self.viewModel, instructionText) 
      initially:^{
        [weakSelf updateFormTitleText:weakSelf.viewModel.instructionText animated:NO]
    }] distinctUntilChanged] 
      subscribeNext:^(id x) {
        [weakSelf updateFormTitleText:x animated:YES];
    ];

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.