Keeping an NSOperation in a queue even after it completes - ios

Typically once the main method of the an NSOperation is completed, the op is marked completed and it is removed from the queue. However, my op makes networking calls, and I want to handle retries. How do I keep an NSOperation in an NSOperationQueue until I explicitly say it's ok to remove it?

I can't find the original source for the work I did on my current project.
I have subclassed NSOperation and do this...
Add private properties in the .m...
#property (nonatomic) BOOL executing;
#property (nonatomic) BOOL finished;
#property (nonatomic) BOOL completed;
Init the operation...
- (id)init
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
_completed = NO;
}
return self;
}
Add the functions to return the properties...
- (BOOL)isExecuting { return self.executing; }
- (BOOL)isFinished { return self.finished; }
- (BOOL)isCompleted { return self.completed; }
- (BOOL)isConcurrent { return YES; }
In the "start" function (this is the bit that the operationQueue calls...
- (void)start
{
if ([self isCancelled]) {
[self willChangeValueForKey:#"isFinished"];
self.finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:#"isExecuting"];
self.executing = YES;
[self didChangeValueForKey:#"isExecuting"];
[NSThread detachNewThreadSelector:#selector(main) toTarget:self withObject:nil];
}
Then in the main put your working code...
- (void)main
{
#try {
//this is where your loop would go with your counter and stuff
//when you want the operationQueue to be notified that the work
//is done just call...
[self completeOperation];
}
#catch (NSException *exception) {
NSLog(#"Exception! %#", exception);
[self completeOperation];
}
}
Write the code for completeOperation...
- (void)completeOperation {
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
self.executing = NO;
self.finished = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
That's it.
As long as you have these then the operation will work.
You can add as many other functions and properties as you wish.
In fact, I have actually subclassed this class as I have a function that does all the work for different types of object (it's an upload thing). I have defined a function...
- (void)uploadData
{
//subclass this method.
}
Then all I have in the subclasses is a custom "uploadData" method.
I find this really useful as it gives you fine grain control on when to finish the operation etc...

Related

Why is NSOperation::completionBlock called too soon?

I have a very simple asynchronous operation
#interface IDBWebpValidationOperation()
#property BOOL executing;
#property BOOL finished;
#end
#implementation IDBWebpValidationOperation
#synthesize executing;
#synthesize finished;
- (instancetype)init
{
self = [super init];
if (self) {
self.completionBlock = ^{
NSDLog(#"webp validation has finished");
};
}
return self;
}
- (void)main
{
IDBAssert0(self.bestCapture.webpCandidate);
self.finished = NO;
if(self.postProcessingValidator) {
self.executing = YES;
// this starts async operation, see callback below
self.postProcessingValidator(self.bestCapture.webpCandidate);
}else{
IDBAssert0(0);
// self.bestCapture.jpegNSData = self.bestCapture.webpCandidate;
IDBAssert0(self.bestCapture.jpegNSData);
self.executing = NO;
self.finished = YES;
}
}
- (void)scanningViewController: (UIViewController<PPScanningViewController>*)scanningViewController
didOutputResults:(NSArray*)results
{
if([results count]>0) {
self.bestCapture.jpegNSData = self.bestCapture.webpCandidate;
IDBAssert0(self.bestCapture.jpegNSData);
}else{
IDBAssert0(self.microblinkFailureHandler);
self.microblinkFailureHandler();
}
IDBAssert0(!self.finished);
self.executing = NO;
self.finished = YES;
}
-(BOOL)isAsynchronous
{
return YES; //Default is NO so overriding it to return YES;
}
Here I have synthesized two atomic properties executing & finished
to avoid lots of (in my opinion dumb & extraneous) code swift would force you to do for KVO and the synthesided atomic properties would get me out of the box
(I think).
The issue is completionBlock is called before scanningViewController:
callback is invoked. Why?????
This is on ios 9.3.x in case this matters

Is it acceptable to daisy NSOperation main into super?

- (void)main
{
IDBAssert0(self.bestCapture.webpCandidate);
self.finished = NO;
self.executing = YES;
NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
UIImage *possiblycorrupted = [UIImage imageWithWebPData:self.bestCapture.webpCandidate];
NSTimeInterval webpInterval = [NSDate timeIntervalSinceReferenceDate]-now;
NSDLog(#"it tooke %.2f sec to unpack webp", webpInterval);
self.microblinkCandidate = possiblycorrupted; // data superclass nsoperation processes
[super main];
}
first thing main in the base class does naturally is setting finished to no and executing to yes:
- (void)main
{
self.finished = NO;
self.executing = YES;
NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
start = now;
CGSize size = [self.microblinkCandidate size];
IDBAssert0(size.width && size.height);
IDBAssert0(self.microblink);
// this starts async processing
[self.microblink processImage:self.microblinkCandidate
scanningRegion:CGRectMake(0.0, 0.0, 1.0, 1.0)
delegate:self];
while (![self isCancelled])
{
sleep(1);
NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
if(now - start > 5) {
// #5677 microblink watchdog to detect hangs
[self cancel];
break;
}
}
[self done];
}
cause it's not an abstract and will be used on its own as well.
the loop is for debug/watchdog purposes only
in the normal operation it's not tripped an operation is done
if this callback:
- (void)scanningViewController: (UIViewController<PPScanningViewController>*)scanningViewController
didOutputResults:(NSArray*)results
{
if([results count]>0) {
NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
NSDLog(#"found barcode in %.1fs", now - start);
self.microblinkSuccessHandler();
}else{
IDBAssert0(self.microblinkFailureHandler);
self.microblinkFailureHandler();
}
[self done];
}
is invoked when "processImage:" will have finished (in a timely fashion).
the very base class is
#implementation IDBAsynchronousOperation
#synthesize executing = _executing;
#synthesize finished = _finished;
-(BOOL)isFinished
{
return _finished;
}
- (void)setFinished:(BOOL)finished
{
[self willChangeValueForKey:#"isFinished"];
_finished = finished;
[self didChangeValueForKey:#"isFinished"];
}
-(BOOL)isExecuting
{
return _executing;
}
- (void)setExecuting:(BOOL)executing
{
[self willChangeValueForKey:#"isExecuting"];
_executing = executing;
[self didChangeValueForKey:#"isExecuting"];
}
- (instancetype)init
{
self = [super init];
if (self) {
// self.completionBlock = ^{
// NSDLog(#"image barcode search has finished");
// };
IDBAssert0(sizeof(_executing)<2);
}
return self;
}
-(BOOL)isAsynchronous
{
return YES;
}
#end
You certainly can (and we often do) subclass your own concrete NSOperation subclass.
To make the base class subclassable, you want to make sure that you only perform self.executing = true once. Right now, the main in both the base class and the subclass do it, and you'll therefore be doing it twice. The typical solution is to pull it out of both of those main implementations and do it in start of the base class. Apple suggests that you do this stuff in start, anyway.
Thus having removed the self.finished and self.executing stuff from both main implementations, you can then implement start:
- (void)start {
if ([self isCancelled]) {
self.finished = YES;
return;
}
self.executing = YES;
[self main];
}
Note, you don't have to call self.finished = false when the operation is starting because that will send an unnecessary KVO.
An unrelated observation:
If you keep the while loop in the base class, I'd suggest exiting the loop if either [self isCancelled] or if the processImage delegate completion methods was called (perhaps you can update some state property to designate when that delegate method was called). Right now, if the processImage finishes before the timeout, it will keep the operation running for the full 5 seconds.
Personally, depending upon how processImage was designed, I probably be inclined excise the while loop entirely. You generally want to avoid any polling like this at all. I might, for example, put the [self done] in the appropriate delegate method and then set up a timer or dispatch_after for the timeout.
- (void)main {
NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
start = now;
CGSize size = [self.microblinkCandidate size];
IDBAssert0(size.width && size.height);
IDBAssert0(self.microblink);
// this starts async processing
[self.microblink processImage:self.microblinkCandidate
scanningRegion:CGRectMake(0.0, 0.0, 1.0, 1.0)
delegate:self];
// cancel upon timeout
typeof(self) __weak weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
typeof(self) __strong strongSelf = weakSelf;
if ([strongSelf isExecuting]) {
[strongSelf cancel];
[strongSelf done]; // if canceling calls the delegate method that calls `done`, then you don't need this here
}
});
}

Read data from NSOperation subclass to multiple viewcontrollers

I will explain scenario.
I have a NSOperation subclass. In this class , I am reading data from multiple bluetooth devices.
I am creating an object of NSOperation class in ViewController A and get data using delegate methods in NSoperation subclass.
Now, I want to read data from Viewcontroller B without creating an object of NSoperation.
Please check my NSOperation Subclass
NOPerationSubclass.h
`
#protocol NOPerationSubclassDelegate`;
#interface NOPerationSubclass : NSOperation{
BOOL executing;
BOOL finished;
}
#property id<NOPerationSubclassDelegate> delegate;
- (id)initWithConnectDevice:(ConnectDevice *)cDevice toPeripheral:(CBPeripheral *)peripheral;
#end
#protocol NOPerationSubclassDelegate
-(void)updateUIFromOperation:(NOPerationSubclass *)operation;
#end
NOPerationSubclass.m
- (id)initWithConnectDevice:(ConnectDevice *)cDevice toPeripheral:(CBPeripheral *)peripheral{
if (self = [super init]) {
executing = NO;
finished = NO;
self.connectDevice = cDevice;
[self.connectDevice setDelegate:self];
self.connectedPeripheral = peripheral;
dataDic = [[NSMutableDictionary alloc] init];
}
return self;
}
-(BOOL)isConcurrent{
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
-(void) terminateOperation {
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
finished = YES;
executing = NO;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
- (void)start {
#autoreleasepool {
if (self.isCancelled){
[self willChangeValueForKey:#"isFinished"];
finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(timerFired:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
-(void)timerFired:(id)sender{
if (self.isCancelled){
[self willChangeValueForKey:#"isFinished"];
finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
[connectDevice calldiscoverServicesForPeripheral:connectedPeripheral];
}
-(void)getDataFromPeripheral:(CBPeripheral *)peripheral Data:(NSString *)data{
[dataDic setValue:[peripheral.identifier UUIDString] forKey:#"identifier"];
[dataDic setValue:data forKey:#"data"];
[[[AppDelegate app] devicesDataArray] addObject:dataDic];
[(NSObject *)self.delegate performSelectorOnMainThread:#selector(updateUIFromOperation:) withObject:dataDic waitUntilDone:NO];
NSLog(#"PERIPHERAL DATA::+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++%#",peripheral.name);
}
And, I am calling this NSOpeartion class from ViewController A like this
NOPerationSubclass *queue = [[NOPerationSubclass alloc] initWithConnectDevice:connectDevices toPeripheral:peripheral];
queue.delegate = self;
[[[AppDelegate app] mainOperationQueue] addOperation:queue];
You can use a shared instance class, this is what I always do:
Database.h
#import <Foundation/Foundation.h>
#interface Database : NSObject
#property (nonatomic, readonly) NSArray* myTable;
+(Database*) sharedInstance;
#end
Database.m
#import "Database.h"
#implementation Database
Database* _db = nil;
+(Database*) sharedInstance {
if (!_db)
_db = [[Database alloc] init];
return _db;
}
-(id) init {
self = [super init];
// Do loading here
return self;
}
#end
Then whenever you want to access the data:
[Database sharedInstance].myTable;

NSOperation + setCompletionBlock

I have few different questions about NSOperation and NSOperationQueue and I know guys that yours answers will help me;
I have to load a big amount of images and I have created my own loader based on NSOperation, NSOperationQueue and NSURLConnection (asynchronous loading);
Questions:
If I set maxConcurrentOperationCount (for example 3) for queue (NSOperationQueue), does it mean that only 3 operations performed in the same time even queue has 100 operations?
When I set property maxConcurrentOperationCount for queue sometimes "setCompletionBlock" doesn't work and count (operationCount) only increases; Why?
MyLoader:
- (id)init
{
self = [super init];
if (self) {
_loadingFiles = [NSMutableDictionary new];
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 3;
_downloadQueue.name = #"LOADER QUEUE";
}
return self;
}
- (void)loadFile:(NSString *)fileServerUrl handler:(GetFileDataHandler)handler {
if (fileServerUrl.length == 0) {
return;
}
if ([_loadingFiles objectForKey:fileServerUrl] == nil) {
[_loadingFiles setObject:fileServerUrl forKey:fileServerUrl];
__weak NSMutableDictionary *_loadingFiles_ = _loadingFiles;
MyLoadOperation *operation = [MyLoadOperation new];
[operation fileServerUrl:fileServerUrl handler:^(NSData *fileData) {
[_loadingFiles_ removeObjectForKey:fileServerUrl];
if (fileData != nil) {
handler(fileData);
}
}];
[operation setQueuePriority:NSOperationQueuePriorityLow];
[_downloadQueue addOperation:operation];
__weak NSOperationQueue *_downloadQueue_ = _downloadQueue;
[operation setCompletionBlock:^{
NSLog(#"completion block :%i", _downloadQueue_.operationCount);
}];
}
}
MyOperation:
#interface MyLoadOperation()
#property (nonatomic, assign, getter=isOperationStarted) BOOL operationStarted;
#property(nonatomic, strong)NSString *fileServerUrl;
#property(nonatomic, copy)void (^OnFinishLoading)(NSData *);
#end
#implementation MyLoadOperation
- (id)init
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
}
return self;
}
- (void)fileServerUrl:(NSString *)fileServerUrl
handler:(void(^)(NSData *))handler {
#autoreleasepool {
self.fileServerUrl = fileServerUrl;
[self setOnFinishLoading:^(NSData *loadData) {
handler(loadData);
}];
[self setOnFailedLoading:^{
handler(nil);
}];
self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]
initWithURL:self.url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:25];
[request setValue:#"" forHTTPHeaderField:#"Accept-Encoding"];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
_data = [[NSMutableData alloc] init];
}
}
- (void)main {
#autoreleasepool {
[self stop];
}
}
- (void)start {
[self setOperationStarted:YES];
[self willChangeValueForKey:#"isFinished"];
_finished = NO;
[self didChangeValueForKey:#"isFinished"];
if ([self isCancelled])
{
[self willChangeValueForKey:#"isFinished"];
_finished = YES;
_executing = NO;
[self didChangeValueForKey:#"isFinished"];
}
else
{
[self willChangeValueForKey:#"isExecuting"];
_finished = NO;
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
}
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return _executing;
}
- (BOOL)isFinished {
return _finished;
}
- (void)cancel {
[self.connection cancel];
if ([self isExecuting])
{
[self stop];
}
[super cancel];
}
#pragma mark -NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[_data appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if ([self OnFinishLoading]) {
[self OnFinishLoading](_data);
}
if (![self isCancelled]) {
[self stop];
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
;
if (![self isCancelled]) {
[self stop];
}
}
- (void)stop {
#try {
__weak MyLoadOperation *self_ = self;
dispatch_async(dispatch_get_main_queue(), ^{
[self_ completeOperation];
});
}
#catch (NSException *exception) {
NSLog(#"Exception! %#", exception);
[self completeOperation];
}
}
- (void)completeOperation {
if (![self isOperationStarted]) return;
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
You must start the connection in the Operation's start method, and not in fileServerUrl:handler:.
I would remove this method altogether, and only provide an init method with all required parameters where you can completely setup the operation. Then, in method start start the connection.
Additionally, it's not clear why you override main.
Modifying the state variables _executing and _finished could be more concise and more clear (you don't need to set them initially, since the are already initialized to NO). Only set them in the "final" method completeOperation including KVO notifications.
You also do not need a #try/#catch in stop, since function dispatch_async() does not throw Objective-C exceptions.
Your cancel method is not thread safe, and there are also a few other issues. I would suggest the following changes:
#implementation MyOperation {
BOOL _executing;
BOOL _finished;
NSError* _error; // remember the error
id _result; // the "result" of the connection, unless failed
completion_block_t _completionHandler; //(your own completion handler)
id _self; // strong reference to self
}
// Use the "main thread" as the "synchronization queue"
- (void) start
{
// Ensure start will be called only *once*:
dispatch_async(dispatch_get_main_queue(), ^{
if (!self.isCancelled && !_finished && !_executing) {
[self willChangeValueForKey:#"isExecuting"];
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
_self = self; // keep a strong reference to self in order to make
// the operation "immortal for the duration of the task
// Setup connection:
...
[self.connection start];
}
});
}
- (void) cancel
{
dispatch_async(dispatch_get_main_queue, ^{
[super cancel];
[self.connection cancel];
if (!_finished && !_executing) {
// if the op has been cancelled before we started the connection
// ensure the op will be orderly terminated:
self.error = [[NSError alloc] initWithDomain:#"MyOperation"
code:-1000
userInfo:#{NSLocalizedDescriptionKey: #"cancelled"}];
[self completeOperation];
}
});
}
- (void)completeOperation
{
[self willChangeValueForKey:#"isExecuting"];
self.isExecuting = NO;
[self didChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
self.isFinished = YES;
[self didChangeValueForKey:#"isFinished"];
completion_block_t completionHandler = _completionHandler;
_completionHandler = nil;
id result = self.result;
NSError* error = self.error;
_self = nil;
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
completionHandler(result, error);
});
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if ([self onFinishLoading]) {
[self onFinishLoading](self.result);
}
[self completeOperation];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
if (self.error == nil) {
self.error = error;
}
[self completeOperation];
}
In answer to your questions:
Yes, a maxConcurrentOperationCount of three means that only three will run at a time. Doing network requests like this is perfect example of when you'd want to use maxConcurrentOperationCount, because failure to do so would result in too many network requests trying to run, most likely resulting in some of the connections failing when using a slower network connection.
The main issue here, though, is that you're calling your operation's fileServerUrl method (which is starting the connection) from MyLoader. You've disconnected the request from the operation's start (defeating the purpose of maxConcurrentCount of 3 and possibly confusing the state of the operation).
The start method should be initiating the connection (i.e. don't start the request until one of those three available concurrent operations is available). Furthermore, since you cannot pass the URL and the handler to the start method, you should move your logic that saves those values to a customized rendition of your init method.
There are other minor edits we might suggest to your operation (main not needed, operationStarted is a little redundant, simplify the _executing/_finished handling, etc.), but the starting of the connection in fileServerUrl rather than being initiated by the start method is the key issue.
Thus:
- (id)initWithServerUrl:(NSString *)fileServerUrl
handler:(void(^)(NSData *))handler
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
// do your saving of `fileServerURL` and `handler` here, e.g.
self.fileServerUrl = fileServerUrl;
self.OnFinishLoading:^(NSData *loadData) {
handler(loadData);
}];
[self setOnFailedLoading:^{
handler(nil);
}];
}
return self;
}
- (void)startRequest {
self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:25];
[request setValue:#"" forHTTPHeaderField:#"Accept-Encoding"];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
_data = [[NSMutableData alloc] init];
}
- (void)start {
if ([self isCancelled])
{
[self willChangeValueForKey:#"isFinished"];
_finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
[self setOperationStarted:YES]; // personally, I'd retire this and just reference your `executing` flag, but I'll keep it here for compatibility with the rest of your code
[self willChangeValueForKey:#"isExecuting"];
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
[self startRequest];
}
For the first question, the answer is yes, if set 3 as a max number of operations, only 3 can be running togheter.
The second is bit strange problem and I'm not totally sure that this answer will be correct. When you leave operations to an NSOperationQueue, you can't be sure on which thread they will be executed, this lead a huge problem with async connection. When you start an NSURLConnection as usual you receive the delegate callbacks without a problem, that is because the connection is running on a thread with a living run loop. If you start the connection on a secondary thread, callbacks will be called on that thread, but if you don't keep the run loop alive they will be never received.That's where probably my answer isn't correct, GCD should take care of living run loops, because GCD queues runs on living threads. But if not, the problem could be that operations are started on a different thread, the start method is called, but the callbacks are never called. Try to check if the thread is always the main thread.

Use of delegates in NSOperation

I am trying to make use of CLLocationManager in an NSOperation. As part of this I require the ability to startUpdatingLocation then wait until a CLLocation is received before completing the operation.
At present I have done the following, however the delegate method never seems to be called. Please can someone advise what the issue is?
- (void)main
{
#autoreleasepool {
if (self.isCancelled)
return;
// Record the fact we have not found the location yet
shouldKeepLooking = YES;
// Setup the location manager
NSLog(#"Setting up location manager.");
CLLocationManager *locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
[locationManager startUpdatingLocation];
while (shouldKeepLooking) {
if (self.isCancelled)
return;
// Do some other logic...
}
}
}
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
// None of this ever seems to be called (despite updating the location)
latestLocation = [locations lastObject];
[manager stopUpdatingLocation];
shouldKeepLooking = NO;
}
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.
I think you have two options.
Create a separate thread, with its own run loop, for location services:
#import "LocationOperation.h"
#import <CoreLocation/CoreLocation.h>
#interface LocationOperation () <CLLocationManagerDelegate>
#property (nonatomic, readwrite, getter = isFinished) BOOL finished;
#property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
#property (nonatomic, strong) CLLocationManager *locationManager;
#end
#implementation LocationOperation
#synthesize finished = _finished;
#synthesize executing = _executing;
- (id)init
{
self = [super init];
if (self) {
_finished = NO;
_executing = NO;
}
return self;
}
- (void)start
{
if ([self isCancelled]) {
self.finished = YES;
return;
}
self.executing = YES;
[self performSelector:#selector(main) onThread:[[self class] locationManagerThread] withObject:nil waitUntilDone:NO modes:[[NSSet setWithObject:NSRunLoopCommonModes] allObjects]];
}
- (void)main
{
[self startStandardUpdates];
}
- (void)dealloc
{
NSLog(#"%s", __FUNCTION__);
}
#pragma mark - NSOperation methods
- (BOOL)isConcurrent
{
return YES;
}
- (void)setExecuting:(BOOL)executing
{
if (executing != _executing) {
[self willChangeValueForKey:#"isExecuting"];
_executing = executing;
[self didChangeValueForKey:#"isExecuting"];
}
}
- (void)setFinished:(BOOL)finished
{
if (finished != _finished) {
[self willChangeValueForKey:#"isFinished"];
_finished = finished;
[self didChangeValueForKey:#"isFinished"];
}
}
- (void)completeOperation
{
self.executing = NO;
self.finished = YES;
}
- (void)cancel
{
[self stopStandardUpdates];
[super cancel];
[self completeOperation];
}
#pragma mark - Location Manager Thread
+ (void)locationManagerThreadEntryPoint:(id __unused)object
{
#autoreleasepool {
[[NSThread currentThread] setName:#"location manager"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)locationManagerThread
{
static NSThread *_locationManagerThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_locationManagerThread = [[NSThread alloc] initWithTarget:self selector:#selector(locationManagerThreadEntryPoint:) object:nil];
[_locationManagerThread start];
});
return _locationManagerThread;
}
#pragma mark - Location Services
- (void)startStandardUpdates
{
if (nil == self.locationManager)
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.distanceFilter = 500;
[self.locationManager startUpdatingLocation];
}
- (void)stopStandardUpdates
{
[self.locationManager stopUpdatingLocation];
self.locationManager = nil;
}
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
CLLocation* location = [locations lastObject];
// do whatever you want with the location
// now, turn off location services
if (location.horizontalAccuracy < 50) {
[self stopStandardUpdates];
[self completeOperation];
}
}
#end
Alternatively, even though you're using an operation, you could just run location services on the main thread:
#import "LocationOperation.h"
#import <CoreLocation/CoreLocation.h>
#interface LocationOperation () <CLLocationManagerDelegate>
#property (nonatomic, readwrite, getter = isFinished) BOOL finished;
#property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
#property (nonatomic, strong) CLLocationManager *locationManager;
#end
#implementation LocationOperation
#synthesize finished = _finished;
#synthesize executing = _executing;
- (id)init
{
self = [super init];
if (self) {
_finished = NO;
_executing = NO;
}
return self;
}
- (void)start
{
if ([self isCancelled]) {
self.finished = YES;
return;
}
self.executing = YES;
[self startStandardUpdates];
}
#pragma mark - NSOperation methods
- (BOOL)isConcurrent
{
return YES;
}
- (void)setExecuting:(BOOL)executing
{
if (executing != _executing) {
[self willChangeValueForKey:#"isExecuting"];
_executing = executing;
[self didChangeValueForKey:#"isExecuting"];
}
}
- (void)setFinished:(BOOL)finished
{
if (finished != _finished) {
[self willChangeValueForKey:#"isFinished"];
_finished = finished;
[self didChangeValueForKey:#"isFinished"];
}
}
- (void)completeOperation
{
self.executing = NO;
self.finished = YES;
}
- (void)cancel
{
[self stopStandardUpdates];
[super cancel];
[self completeOperation];
}
#pragma mark - Location Services
- (void)startStandardUpdates
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
if (nil == self.locationManager)
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.distanceFilter = 500;
[self.locationManager startUpdatingLocation];
}];
}
- (void)stopStandardUpdates
{
[self.locationManager stopUpdatingLocation];
self.locationManager = nil;
}
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
CLLocation* location = [locations lastObject];
// do whatever you want with the location
// now, turn off location services
if (location.horizontalAccuracy < 50) {
[self stopStandardUpdates];
[self completeOperation];
}
}
#end
I think I'd be inclined to do the second approach (just making sure that I don't do anything too intensive in didUpdateLocations, or if I did, make sure to do it asynchronously), but both of these approaches appear to work.
Another approach is to keep the run loop alive until the operation is finished:
while (![self isFinished]) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
}
But this doesn't appear to work in conjunction with CLLocationManager, as runUntilDate doesn't immediately return (it's almost as if CLLocationManager is attaching its own source to the runloop, which prevents it from exiting). I guess you could change the runUntilDate to something a little closer than distantFuture (e.g. [NSDate dateWithTimeIntervalSinceNow:1.0]). Still, I think it's just as easy to run this operation start location services on the main queue, like the second solution above.
Having said that, I'm not sure why you would want to use location manager in an operation at all. It's already asynchronous, so I would just start the location manager from the main queue and call it a day.
UIWebView with UIWebViewDelegate method callbacks in an NSOperation
A server I wanted to grab a URL from a server that changes values based upon JavaScript execution from various browsers. So I slapped a dummy UIWebView into an NSOperation and use that to grab out the value I wanted in the UIWebViewDelegate method.
#interface WBSWebViewOperation () <UIWebViewDelegate>
#property (assign, nonatomic) BOOL stopRunLoop;
#property (assign, nonatomic, getter = isExecuting) BOOL executing;
#property (assign, nonatomic, getter = isFinished) BOOL finished;
#property (copy, nonatomic, readwrite) NSURL *videoURL;
#property (strong, nonatomic) UIWebView *webView;
#end
#implementation WBSWebViewOperation
- (id)init
{
self = [super init];
if (self) {
_finished = NO;
_executing = NO;
}
return self;
}
- (id)initWithURL:(NSURL *)episodeURL
{
self = [self init];
if (self != nil) {
_episodeURL = episodeURL;
}
return self;
}
- (void)start
{
if (![self isCancelled]) {
self.executing = YES;
[self performSelector:#selector(main) onThread:[NSThread mainThread] withObject:nil waitUntilDone:NO modes:[[NSSet setWithObject:NSRunLoopCommonModes] allObjects]];
} else {
self.finished = YES;
}
}
- (void)main
{
if (self.episodeURL != nil) {
NSURLRequest *request = [NSURLRequest requestWithURL:self.episodeURL];
UIWebView *webView = [[UIWebView alloc] init];
webView.delegate = self;
[webView loadRequest:request];
self.webView = webView;
}
}
#pragma mark - NSOperation methods
- (BOOL)isConcurrent
{
return YES;
}
- (void)setExecuting:(BOOL)executing
{
[self willChangeValueForKey:#"isExecuting"];
_executing = executing;
[self didChangeValueForKey:#"isExecuting"];
}
- (void)setFinished:(BOOL)finished
{
[self willChangeValueForKey:#"isFinished"];
_finished = finished;
[self didChangeValueForKey:#"isFinished"];
}
- (void)completeOperation
{
self.executing = NO;
self.finished = YES;
}
- (void)cancel
{
[self.webView stopLoading];
[super cancel];
[self completeOperation];
}
#pragma mark - UIWebViewDelegate methods
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSString *episodeVideoURLString = [webView stringByEvaluatingJavaScriptFromString:#"document.getElementById('playerelement').getAttribute('data-media')"];
NSURL *episodeVideoURL = [NSURL URLWithString:episodeVideoURLString];
self.videoURL = episodeVideoURL;
if ([self.delegate respondsToSelector:#selector(webViewOperationDidFinish:)]) {
[self.delegate webViewOperationDidFinish:self];
}
[self completeOperation];
}
#end
Its going to call the delegate method in the same operation queue as main is running in. And NSOperation queues are serial by default. Your while loop is just spinning forever (because the operation is never cancelled) and the call to your delegate method is sitting in the queue behind it never able to run.
Get rid of the while loop entirely and let the operation finish. Then when the delegate method is called, if it's cancelled discard the result by returning.

Resources