Ash Furrow
Ash Furrow

Reputation: 12421

Perform action on the hour, every hour, with ReactiveCocoa

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"?

Upvotes: 11

Views: 2995

Answers (2)

Justin Spahr-Summers
Justin Spahr-Summers

Reputation: 16973

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.

Upvotes: 20

Dave Lee
Dave Lee

Reputation: 6489

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
}];

Upvotes: 5

Related Questions