11

Trying to follow the best practices of ReactiveCocoa to update my UI on the hour, every hour. This is what I've got:

NSDateComponents *components = [[[NSCalendar sharedCalendar] calendar] components:NSMinuteCalendarUnit fromDate:[NSDate date]];
// Generalization, I know (not every hour has 60 minutes, but bear with me).
NSInteger minutesToNextHour = 60 - components.minute;

RACSubject *updateEventSignal = [RACSubject subject];
[updateEventSignal subscribeNext:^(NSDate *now) {
    // Update some UI
}];

[[[RACSignal interval:(60 * minutesToNextHour)] take:1] subscribeNext:^(id x) {
    [updateEventSignal sendNext:x];
    [[RACSignal interval:3600] subscribeNext:^(id x) {
        [updateEventSignal sendNext:x];
    }];
}];

This has some obvious flaws: manual subscription and sending, and it just "feels wrong." Any ideas on how to make this more "reactive"?

Ash Furrow
  • 12,391
  • 3
  • 57
  • 92

2 Answers2

20

You can do this using completely vanilla operators. It's just a matter of chaining the two intervals together while still passing through both of their values, which is exactly what -concat: does.

I would rewrite the subject as follows:

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

This may not give you super ultra exact precision (because there might be a minuscule hiccup between the two signals), but it should be Good Enough™ for any UI work.

Justin Spahr-Summers
  • 16,893
  • 2
  • 61
  • 79
5

Sounds like you need something like +interval:startingIn:.

With that thought, you could make your own version of +interval:startingIn: by slightly tweaking the implementation of +interval:.

+ (RACSignal *)interval:(NSTimeInterval)interval startingIn:(NSTimeInterval)delay {
  return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {

    int64_t intervalInNanoSecs = (int64_t)(interval * NSEC_PER_SEC);
    int64_t delayInNanoSecs = (int64_t)(delay * NSEC_PER_SEC);
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, delayInNanoSecs), (uint64_t)intervalInNanoSecs, (uint64_t)0);
    dispatch_source_set_event_handler(timer, ^{
      [subscriber sendNext:[NSDate date]];
    });
    dispatch_resume(timer);

    return [RACDisposable disposableWithBlock:^{
      dispatch_source_cancel(timer);
      dispatch_release(timer);
    }];
  }] setNameWithFormat:@"+interval: %f startingIn: %f", (double)interval, (double)delay];
}

With this in place, your code could be refactored to:

NSDateComponents *components = [[[NSCalendar sharedCalendar] calendar] components:NSMinuteCalendarUnit fromDate:[NSDate date]];
// Generalization, I know (not every hour has 60 minutes, but bear with me).
NSInteger minutesToNextHour = 60 - components.minute;

RACSubject *updateEventSignal = [[RACSignal interval:3600 startingIn:(minutesToNextHour * 60)];
[updateEventSignal subscribeNext:^(NSDate *now) {
    // Update some UI
}];
Dave Lee
  • 6,299
  • 1
  • 36
  • 36
  • This is clever, but we really recommend [composing existing operators](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/Documentation/DesignGuidelines.md#compose-existing-operators-when-possible) whenever possible, instead of writing new ones from scratch. Among other things, that means you'll get any bug fixes for free. – Justin Spahr-Summers May 14 '13 at 05:39
  • I realized I was going astray of the recommendations, but old habits die hard. Thanks for the gentle reminder. – Dave Lee May 14 '13 at 16:01
  • No worries! You can always submit a pull request if you think it should be standard, too. :) – Justin Spahr-Summers May 14 '13 at 18:03