Question

I'm building a "monitoring" app on my iPhone. I'm using AFNetworking-2.0. I have a backend server exposing a RESTful interface written in Python3/tornado.

Depending on what level of ViewController I'm at, I want to poll different data with different queries (the focus of the application tunes the focus of the queries). In the interest of "Make it Work", I have set up the following:

#pragma mark - Pull Loop

- (void) forkPull {
    NSString* uri = [NSString stringWithFormat: @"%@/valves",  Site.current.serialID];
    [[HttpConnection current]
        GET: uri
        parameters: @{}
        success:^(NSURLSessionDataTask* task, id responseObject){
            [Site.current performSelectorOnMainThread: @selector(fromDoc:) withObject:responseObject waitUntilDone:YES];
            NSTimeInterval delay = 60; // default poll period
            // attempt to hone in if we have valid lastTouch info
            if (Site.current.touched != nil) {
                NSDate *futureTick = [Site.current.touched dateByAddingTimeInterval: 65];
                if ([futureTick compare: [NSDate date]] == NSOrderedDescending) {
                    delay = futureTick.timeIntervalSinceNow;
                }
            }
            [self performSelector: @selector(forkPull) withObject:nil afterDelay:delay];
            NSLog(@"%@ forkPull again in %f", self, delay);
        }
        failure:^(NSURLSessionDataTask* task, NSError* error){
            NSLog(@"%@ forkPull error: %@ (uri=%@)", self, error, uri);
            [self performSelector: @selector(forkPull) withObject:nil afterDelay:60];
        }
    ];
}

- (void) stopPull {
    [NSObject cancelPreviousPerformRequestsWithTarget: self];
}

#pragma mark - View Management

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear: animated];
    ....
    [self forkPull]; // start up polling while I'm visible
}

-(void) viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self stopPull]; // I'm going away, so shut down the pull loop?
    ...
}

Basically, when the controller's view appears, it sends a REST query (when it gets back asynchronously, it will update the model in the fromDoc: methods; the controller has KVO relationships set up which will cause UI changes. After the update completes, it's able to approximate about when it should make the next pull, and schedules that with performSelector:withObject:afterDelay:. When another controller takes center stage, the viewWillDisappear: method attempts to stop any forkPulls that have been queued.

While this kinda works. I'm pretty sure it doesn't pass the "Make it Right" test. I'm naive about how all the tasks and backgrounding work, but it seems to me that AFNetworking adds its own level of them, so my stopPull might not be effective. I've seen some evidence of that with my NSLog output, where it seems that controllers that aren't on the top anymore, still have loops running.

But I'm sure others have done this kind of pattern before. I'd love to know how to better architect/implement this. I'm looking for someone to share the pattern they've used for doing the semi-periodic REST queries, that has been vetted and works well.

Was it helpful?

Solution

Use Grand Central Dispatch:

@property (strong, nonatomic) dispatch_source_t timer;

- (void)startTimer
{
    if (!self.timer) {
        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    }
    if (self.timer) {
        dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, 0), 60ull*NSEC_PER_SEC, 10ull*NSEC_PER_SEC);
        dispatch_source_set_event_handler(_timer, ^(void) {
            [self tick];
        });
        dispatch_resume(_timer);
    }
}

- (void)tick
{
    // Do your REST query here
}

This will invoke your tick method every 60 seconds.

To suspend and resume your timer, use dispatch_suspend and dispatch_resume:

dispatch_suspend(self.timer);
dispatch_resume(self.timer);

You can invoke dispatch_source_set_timer at any time later to schedule ticks sooner or delay them until later:

// Fire sooner than 60 seconds, but resume 60s fires after that
unsigned long long delaySeconds = arc4random() % 60;
dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, delaySeconds * NSEC_PER_SEC), 60ull*NSEC_PER_SEC, 10ull*NSEC_PER_SEC);

See the Apple Concurrency Programming Guide for full docs on this.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top