NSTimer in NSOperation

Using NSTimer and NSRunLoop. This technique requires storing the thread and the timer references. The thread is stored because in this example, the invalidate method could is called from a different thread. It will cause retention issues if you fail to do this.

Calling (the invalidate) method requests the removal of the timer from the current run loop; as a result, you should always call the invalidate method from the same thread on which the timer was installed.

This example adds the timer to the current runloop.

NSTimer Class References

//
//  GetLocationCommand.m
//  Buzzrd
//
//  Created by Brian Mancini on 6/29/14.
//  Copyright (c) 2014 Buzzrd. All rights reserved.
//

#import "GetLocationCommand.h"

@interface GetLocationCommand()

@property (strong, nonatomic) NSTimer *timeoutTimer;
@property (strong, nonatomic) NSThread *timerThread;

@end

@implementation GetLocationCommand {
    bool executing;
    bool finished;
}

- (id)init
{
    self = [super init];
    if(self) {
        self.completionNotificationName = @"getLocationComplete";
        executing = false;
        finished  = false;
    }
    return self;
}


// iOS8 support for asynchronous NSOperations
- (bool) isAsynchronous {
    return true;
}


// iOS7 support for asynchronous NSOperations
- (bool) isConcurrent {
    return true;
}


// Override for isExecuting, required by async NSOperations
- (bool) isExecuting {
    return executing;
}


// Overrivde for isFinished, required by async NSOperations
- (bool) isFinished {
    return finished;
}


// Required by async NSOperations
// This will perform KVO for isExecuting property and call main
- (void) start {
    NSLog(@"%p:GetLocationCommand:start", self);
    
    [self willChangeValueForKey:@"isExecuting"];
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    executing = true;
    [self didChangeValueForKey:@"isExecuting"];
}


// Main NSOperation code
- (void) main {
    NSLog(@"%p:GetLocationCommand:main", self);
    
    // start timeout mechanism
    self.timerThread = [NSThread currentThread];
    self.timeoutTimer = [NSTimer timerWithTimeInterval:15.0 target:self selector:@selector(timeout) userInfo:nil repeats:false];
    [[NSRunLoop currentRunLoop] addTimer:self.timeoutTimer forMode:NSDefaultRunLoopMode];
    NSLog(@"  -> Scheduled timer %p", self.timeoutTimer);
    
    // add event handlers
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(locationUpdated:) name:BZLocationManagerUpdated object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(locationErrored:) name:BZLocationManagerErrored object:nil];
    
    // start the location request
    [[BZLocationManager instance] requestLocation];
    
    // wait for timeout
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}


// Fires on deallocation
- (void)dealloc {
    NSLog(@"%p:GetLocationCommand:dealloc", self);
}


// Fires when after the elapsed timeout period
- (void)timeout {
    NSLog(@"%p:GetLocationCommand:timeout", self);
    
    CLLocation *lastLocation = [[BZLocationManager instance] requestLastLocation];
    
    if(lastLocation) {
        NSLog(@"  -> Sending last location: (%f, %f)", lastLocation.coordinate.longitude, lastLocation.coordinate.latitude);
        [self sendSuccess:lastLocation];
    } else {
        NSLog(@"  -> Sending error");
        [self  sendError:[[NSError alloc] init]];
    }
    
    [self shutdownCommand];
}


// Event handler for BZLocationManager errors
- (void)locationErrored:(NSNotification *)notification {
    NSLog(@"%p:GetLocationCommand:locationErrored", self);
        
    NSError *error = notification.userInfo[BZLocationManagerErroredErrorInfoKey];
    [self sendError:error];
    [self shutdownCommand];
}


// Event handler for BZLocationManager updates
- (void)locationUpdated:(NSNotification *)notification {
    NSLog(@"%p:GetLocationCommand:locationUpdated", self);
    
    CLLocation *location = notification.userInfo[BZLocationManagerUpdatedLocationInfoKey];
    [self sendSuccess:location];
    [self shutdownCommand];
}


// Triggers an error for the NSOperation
- (void)sendError:(NSError*)error {
    self.status = kFailure;
    self.results = error;
    [self sendCompletionFailureNotification];
}


// Triggers a succses for the NSOperation
- (void)sendSuccess:(CLLocation *)location {
    self.status = kSuccess;
    self.results = location;
    [self sendCompletionNotification];
}


// Shuts down the NSOperation
- (void)shutdownCommand {
    NSLog(@"%p:GetLocationCommand:shutdownCommand", self);
    
    // Clear out observers
    [[NSNotificationCenter defaultCenter] removeObserver:self];

    // Invalidate the timeout
    [self invalidateTimeout];
    
    // Execute KVO for isExecuting and isFinished properties
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
    
    executing = false;
    finished = true;
    
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

// Invalidates the timer used for timeout handling
- (void)invalidateTimeout {
    NSLog(@"%p:GetLocationCommand:invalidateTimeout", self);
    
    [self performSelector:@selector(doInvalidateTimeout) onThread:self.timerThread withObject:nil waitUntilDone:true];
}

- (void)doInvalidateTimeout {
    
    // using NSTimer
    NSLog(@"  -> Invalidating timer %p", self.timeoutTimer);
    [self.timeoutTimer invalidate];
    self.timeoutTimer = nil;
}

@end