I am trying to make use of CLLocationManager
in an NSOperation
. As part of this I require the ability to startUpdatingLocation
then wait u
Going back to the runloop discussion, this is how I generally solve that in my base NSOperation
implementation:
// create connection and keep the current runloop running until
// the operation has finished. this allows this instance of the operation
// to act as the connections delegate
_connection = [[NSURLConnection alloc] initWithRequest:[self request]
delegate:self];
while(!self.isFinished) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
}
I key off of isFinished
, which I keep updated through setters for isCancelled
and isFinished
. Here's the isCancelled
setter as an example:
- (void)setIsCancelled:(BOOL)isCancelled {
_isCancelled = isCancelled;
if (_isCancelled == YES) {
self.isFinished = YES;
}
}
That said, I second some of the questions about why this is necessary. If you don't need to kick something off until a location is found, why not just fire up your location manager on the main thread, wait for the appropriate delegate callback and then kick off the background operation?
Update: updated solution
While the original answer generally stands, I've fully implement a solution and it does require a slight change to how you manage the run loop. That said, all code is available on GitHub - https://github.com/nathanhjones/CLBackgroundOperation. Here is a detailed explanation of the approach.
Tl;dr
Change
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
to
[[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes
beforeDate:[NSDate distantFuture]];
Details
Within your operations interface define the following three properties. We'll be indicating that these operations are concurrent thus we'll manage their state manually. In the solution on GitHub these are part of NJBaseOperation
.
@property(nonatomic,assign,readonly) BOOL isExecuting;
@property(nonatomic,assign,readonly) BOOL isFinished;
@property(nonatomic,assign,readonly) BOOL isCancelled;
Within your operations implementation you'll want to make those readwrite like so:
@interface NJBaseOperation ()
@property(nonatomic,assign,readwrite) BOOL isExecuting;
@property(nonatomic,assign,readwrite) BOOL isFinished;
@property(nonatomic,assign,readwrite) BOOL isCancelled;
@end
Next, you'll want to synthesize the three properties you defined above so that you can override the setters and use them to manage your operations state. Here's what I generally use, but sometimes there are some additional statements added to the setIsFinished:
method depending on my needs.
- (void)setIsExecuting:(BOOL)isExecuting {
_isExecuting = isExecuting;
if (_isExecuting == YES) {
self.isFinished = NO;
}
}
- (void)setIsFinished:(BOOL)isFinished {
_isFinished = isFinished;
if (_isFinished == YES) {
self.isExecuting = NO;
}
}
- (void)setIsCancelled:(BOOL)isCancelled {
_isCancelled = isCancelled;
if (_isCancelled == YES) {
self.isFinished = YES;
}
}
Lastly, just so that we don't have to manually send the KVO notifications we'll implement the following method. This works because our properties are named isExecuting
, isFinished
and isCancelled
.
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
return YES;
}
Now that the the operations foundation is taken care of it's time to knockout the location stuff. You'll want to override main
and within it fire up your location manager and instruct the current run loop to keep running until you tell it otherwise. This ensures that your thread is around to receive the location delegate callbacks. Here's my implementation:
- (void)main {
if (_locationManager == nil) {
_locationManager = [[CLLocationManager alloc] init];
_locationManager.delegate = self;
_locationManager.desiredAccuracy = kCLLocationAccuracyBest;
[_locationManager startUpdatingLocation];
}
while(!self.isFinished) {
[[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes
beforeDate:[NSDate distantFuture]];
}
}
You should receive a delegate callback at which point you can do some work based on location and then finish the operation. Here's my implementation that counts to 10,000 and then cleans up.
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
NSLog(@"** Did Update Location: %@", [locations lastObject]);
[_locationManager stopUpdatingLocation];
// do something here that takes some length of time to complete
for (int i=0; i<10000; i++) {
if ((i % 10) == 0) {
NSLog(@"Loop %i", i);
}
}
self.isFinished = YES;
}
The source on GitHub includes a dealloc
implementation, which simply logs that it's being called and also observes changes to the operationCount
of my NSOperationQueue
and logs the count - to indicating when it drops back to 0. Hope that helps. Let me know if you've got questions.