Searching using NSOperation - iphone

I am trying to make a searchbar to search for things I receive using NSURLConnection.
right now, if I search for something, that string is send away as an URL with an asynchronous request, which gives me data.
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:20.0];
[theConnection cancel];
[theConnection release];
theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
That data is parsed and when it is successful I post a notification
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
xmlParser = [[NSXMLParser alloc] data];
[xmlParser setDelegate:xmlGeocoder];
BOOL success = [xmlParser parse];
if(success == YES){
NSLog(#"No Errors");
[[NSNotificationCenter defaultCenter] postNotificationName:#"getArray" object:self];
}else{
NSLog(#"Error Error Error!!!");
[[NSNotificationCenter defaultCenter] postNotificationName:#"failToGetArray" object:self];
}
}
and my searchresultsTableView is reloaded.
self.array1 = [array2 copy];
[self.searchDisplayController.searchResultsTableView reloadData];
All these methods are depending on eachother, so B can't be executed, when A is still busy.
I am using NSNotificationCenter to tell them to execute those code.
But I want to try NSOperation and I have no idea HOW to implement that.
Do I have to put my search requests in an operation or every method I'm using?
Can someone give me a sample code to give me the idea how this should be done?
Thanks in advance...

NSOperation is very useful. To use it you extend NSOperation and override the "main" method.
In the main method you do your calculations/web request etc. So NSOperation is best for tasks you can wrap into a few simple steps, after each step you test if everything is good and either continue to the next step or cancel the operation. Once this is done you can simply instantiate your custom NSOperation and hand it off to a NSOperationQueue object and it will take care of the threading, starting, stopping cleaning up etc.
In the example below I have written a protocol to handle the completion of the task, I would advise you take this approach instead of using notification - unless you have multiple objects that needs to be notified instantly.
Make a new class that extends the NSOperation class:
//This object takes a "searchTerm" and waits to be "started".
#import <Foundation/Foundation.h>
#protocol ISSearchOperationDelegate
- (void) searchDataReady:(NSArray*) searchResult;
#end
#interface ISSearchOperation : NSOperation {
id <ISSearchOperationDelegate> delegate;
NSString *searchTerm;
}
#property(nonatomic, retain) NSString *searchTerm;
#property(nonatomic, assign) id delegate;
- (id) initWithSearchTerm:(NSString*) searchString;
#end
When an object extending NSOperation is added to an NSOperationQueue, the queue object
tries to call a "main" method on the NSOperation, you must therefore wrap your task in this method.
(notice that after each completed sub-task I test if it went well and "return" if not. The NSOperation class
has a property called isCancelled This property can be set by the NSOperationQueue, so you must also
test if that has been set during your completion of main. So to recap, you test from the inside of main if each step went as you wanted and you test if something on the outside has cancelled your task.):
- (id) initWithSearchTerm:(NSString*) searchString {
if (self = [super init]) {
[self setSearchTerm:searchString];
}
return self;
}
- (void) main {
[self performSelector:#selector(timeOut) withObject:nil afterDelay:4.0];
if ([self isCancelled]) return;
NSData *resultData = [self searchWebServiceForString:self.searchTerm];
if (resultData == nil) return;
if ([self isCancelled]) return;
NSArray *result = [self parseJSONResult:resultData];
if ([self isCancelled]) return;
if (result == nil) return;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[delegate performSelectorOnMainThread:#selector(searchDataReady:) withObject:result waitUntilDone:YES];
}
//I have not copied the implementation of all the methods I call during main, but I hope you understand that they are just "tasks" that each must be successfully completed before the next sub-task can be computed.
So first of I put a timeout test in there, then I get my data from the web service and then I parse it.
Ok to get all this going you need a queue.
So in the class you want to be the delegate for this operation you do this:
somewhere set up a queue:
NSOperationQueue *q = [[NSOperationQueue alloc] init];
[self setQueue:q];
[q release];
- (void) doSearch:(NSString*) searchString {
[queue cancelAllOperations];
ISSearchOperation *searchOperation = [[ISSearchOperation alloc] initWithSearchTerm:searchString];
[searchOperation setDelegate:self];
[queue addOperation:searchOperation]; //this makes the NSOperationQueue call the main method in the NSOperation
[searchOperation release];
}
//the delegate method called from inside the NSOperation
- (void) searchDataReady:(NSArray*) results {
//Data is here!
}
Some of the advantages with NSOperations is that from the caller point of view, we simply make an object, set a delegate, wait for the reply. But behind the scenes a series of threaded tasks that can be cancelled at any time is run, and in a manner that can handle if threaded stuff fails.
As you can see in the doSearch method it starts out by canceling any previous operations, I did this in an app where I would search a web service each time a user typed a letter in a word. That means that if the user searched for "hello world" - I would do a search for "h", then "he", then "hel", then hell", then "hello" etc.
I wanted to stop and clean up the "h" task as soon as the user typed the "e", because it was then obsolete.
I found out NSOperation was the only way that gave the responsiveness of threading and none of the mess that usually comes with spawning many threads on top of each other.
Hope you can use it to get started:)

Related

(iphone) having NSThread* as member variable is a bad idea?

One of my custom class has NSThread* as member variable.
I'm letting the thread exit by setting a variable(isNeedToExit).
Then call a release on the thread object [myThread release];
I intended the thread to exit first and release call gets called.
But on second thought, release call might get called before the thread notices the boolean value changed and exit.
(Because the thread is not constantly looking at the value to see if it needs to exit)
#property (nonatomic, retain) NSThread* myThread;
- (void) dealloc
{
...
[myThread release];
myThread = nil;
...
[super dealloc];
}
- (id) initMyClass
{
if(self = [super init] )
{
...
NSThread* aThread = [[NSThread alloc] initWithTarget: self selector:#selector(theThreadMain) object: nil];
self.myThread = aThread;
[aThread release];
[self.myThread start];
..
}
return self;
}
- (void) theThreadMain
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
// Add your sources or timers to the run loop and do any other setup.
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
do
{
// Start the run loop but return after each source is handled.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
}
while (self.isNeedToExit == false);
[pool release];
SYSLOG(LOG_DEBUG, "thread exiting");
}
I thought of moving [myThread release] call (now at the class's dealloc) to last line of theThreadMain..
Not sure if this is the correct way to stop a thread when it is a member variable of another class.
Thank you
Actually, your initial assumption was more correct. Your current implementation, however, will not work because dealloc will never be called.
Your class is retained by NSThread in initWithTarget:selector:object and will not be released until the thread finishes executing. Therefore, your class instance will not be deallocated as long as the thread is executing.
One way to solve the problem is to add a "cancel" method to your class that stops the thread. E.g.:
- (void)cancel
{
isNeedToExit = YES;
[myThread release];
myThread = nil;
}
That will cause the thread to stop and will allow your class to be deallocated (once the thread stops.) You need to ensure that all users of your class know that "cancel" needs to be called before the class is released. E.g.:
[myObject cancel]; // Stop the thread
[myObject release];
myObject = nil;
A more traditional solution would be to simply subclass NSThread.

problem with delegate of object which is delegate of NSURLConnection

I've a class PictureDownloader for the purpose of asynchronously loading images from a server. It assigns itself as a delegate of NSURLConnection and as such, is retained by NSURLConnection. I create several of those PictureDownloader in a DetailViewController to fetch the corresponding images, so the DetailViewController is a delegate of each PictureDownloader.
When the user leaves the DetailViewController, all remaining downloads are cancelled, however sometimes it seems to be the case, that a PictureDownloader has finished loading an image (connectionDidFinishedLoading called) before the connection was cancelled, but the DetailViewController doesn't exist anymore (but the PictureDownloader does, because it's retained by NSURLConnection), so the call
[self.delegate didLoadPictureWithID:self.ID];
inside PictureDownloader will give an EXC_BAD_ACCESS or sometimes a "unrecognized selector sent to instance".
Here are the relevant parts of the source code:
creation of the PictureDownloader inside the DetailViewController
- (void)startPictureDownload:(Picture *)pic withPictureId:(NSString *)pId forID:(int)ID
{
PictureDownloader *downloader = [self.downloadsInProgress objectForKey:[NSNumber numberWithInt:ID]];
if(!downloader)
{
downloader = [[PictureDownloader alloc] init];
downloader.picture = pic;
downloader.pictureId = pId;
downloader.ID = ID;
downloader.delegate = self;
[self.downloadsInProgress setObject:downloader forKey:[NSNumber numberWithInt:ID]];
[downloader startDownload];
[downloader release];
}
}
canceling the downloads (called when the DetailViewController returns to the overview)
- (void)cancelAllDownloads
{
[self.downloadsInProgress enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop){
[obj cancelDownload];
}];
}
delegate method which is called when PictureDownloader finished loading
- (void)didLoadPictureWithID:(int)dID;
{
PictureDownloader *downloader = [self.downloadsInProgress objectForKey:[NSNumber numberWithInt:dID]];
if(downloader)
{
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:dID];
imageView.image = [UIImage imageWithData:downloader.imageData];
[self.downloadsInProgress removeObjectForKey:[NSNumber numberWithInt:dID]];
}
}
cancelDownload method inside PictureDownloader
- (void)cancelDownload
{
[self.imageConnection cancel];
self.imageConnection = nil;
self.imageData = nil;
}
connectionDidFinishedLoading inside PictureDownloader
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if(self.picture)
{
self.picture.data = self.imageData;
NSError *error = nil;
[self.picture.managedObjectContext save:&error];
}
if(self.delegate != nil && [self.delegate respondsToSelector:#selector(didLoadPictureWithID:)] ) //place of failure
[self.delegate didLoadPictureWithID:self.ID];
self.imageData = nil;
self.imageConnection = nil;
}
Can someone give me a hint, how I can deal with this problem?
Help is much appreciated.
To avoid situations like this, I usually add a check like this at the top of connectionDidFinishLoading: and other NSURLConnection delegate methods:
if (connection != self.imageConnection) return;
As another option, you could set the delegate on each PictureDownloader to nil as you cancel it in cancelAllDownloads. Or you could set self.delegate = nil in cancelDownload.
You should check for the existence of the delegate object (and ideally the method/selector) before you attempt to make the call.
For example:
if(self.delegate && [[self.delegate] respondsToSelector:#selector(didLoadPictureWithID:)]) {
...
}
By doing this, you'll ensure that your not attempting to call a delegate that's no longer there. For more information on the respondsToSelector method, see the NSObject Protocol Reference.
When your DetailViewController goes out of scope - dealloc -, set the PictureDownloader's delegate property to nil as well.
Your issue is interesting in that the NSUrlConnection's delegate can't be set to nil in the same way. e.g. When you're PictureDownloader is de-alloced. All you can do is cancel the NSUrlConnection.
The NSURLConnection docs say the following:
Unless a NSURLConnection receives a cancel message, the delegate will receive one and only one of connectionDidFinishLoading:, or connection:didFailWithError: message, but never both. In addition, once either of messages are sent, the delegate will receive no further messages for the given NSURLConnection.
Indicating that the you can verify that the delegate will not be called back after the above messages are received.

how to properly use autoreleasepool for an nsoperationqueue

I have an app that I am refactoring and I just implemented multithreading so that the UI may run smoother. In the iphone simulator I don't get any leaks but testing on my iPhone 3G running on iOS 4.2 I get a memory leak. I have done a lot of searching for the correct way to implement an autoreleasepool with an operationqueue, help will be greatly appreciated.
I have created an nsoperationqueue in my viewcontroller as such
- (void)loadData
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSOperationQueue *queue = [NSOperationQueue new]; // creates multithread for loading data without slowing UI
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(firstRun) object:nil];
[queue addOperation:operation];
[operation release];
[queue release];
[pool release];
}
First, you shouldn't just create and then release the queue. It's more natural to create that queue as one of your class's ivars and then release it when your view controller goes away (you can also cancel up any pending operations and cancel/wait for any running operations to complete).
Second, you don't need the autorelease pool in the method that creates the operation and adds it in the queue since that method is being called from the main thread. Instead, you need the autorelease pool from the method your object actually calls (this is what may be running on another thread).
So, you might have the following (assuming you name your queue ivar queue_):
- (void)viewDidLoad
{
[super viewDidLoad];
if( !queue_ ) queue_ = [[NSOperationQueue alloc] init];
// other view loading code
}
- (void)loadData
{
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(firstRun) object:nil];
[queue_ addOperation:operation];
[operation release];
}
- (void)firstRun
{
// Here we may run on another thread, so 1) we need an autorelease pool; and
// 2) we need to make sure we don't do anything that requires a runloop
NSAutoreleasePool* threadPool = [NSAutoreleasePool new];
// do long-running things
[threadPool drain];
}
- (void)dealloc
{
if( queue_ ) {
[queue_ setSuspended:YES];
[queue_ cancelAllOperations];
// you need to decide if you need to handle running operations
// reasonably, but don't wait here because that may block the
// main thread
[queue_ release];
}
// other dealloc stuff
[super dealloc];
}
You could also initialize your queue on demand, so instead of initializing in viewDidLoad, check for its existence and initialize if necessary anywhere you'd add an operation. This might be wrapped in a method call of its own, but lazy initialization here probably isn't really necessary as queues are pretty light-weight.
You should create an NSAutoreleasePool at the start of the method that the NSOperation will invoke (in this case, firstRun), and drain it at the end of the method.

Cancel NSOperation in for loop?

I am trying to implement search on a background thread using NSOperation on iOS. I didn't want to subclass NSOperation so this is what I'm doing:
[searchQueue cancelAllOperations];
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self
elector:#selector(filterContentForSearchText:)
object:self.searchDisplayController.searchBar.text];
[searchQueue addOperation:op];
[op release];
The search method includes a for loop that checks whether what is being searched is in an array. Now when I cancel the NSOperation by calling cancelAllOperations, the for loop continues to run through the array. I would like to prevent this and was wondering whether it is legit to call this from within the for loop:
if ([[[searchQueue operations] objectAtIndex:0] isCancelled]) {
[tmp_array release]; // tmp_array is used to hold temporary results
[pool drain]; // pool is my autorelease pool
return;
}
One of the reasons to subclass NSOperation is to implement proper cancellation. You could do your approach, but it violates several good design principles. Basically, since cancellation requires the cooperation of the operation itself, NSInvocationOperation isn't built to cancel the invocation while it's already executing (though it can be successfully cancelled before it starts executing), as the running method shouldn't know anything about how it's called.
Instead, if you subclass NSOperation, you can put most of this functionality into the main method very easily:
#implementation MyOperation
- (void)main {
if ([self isCancelled])
return;
for (...) {
// do stuff
if ([self isCancelled]) {
[tmp_array release];
return;
}
}
}
#end
Note also that you don't have to maintain your own autorelease pool with such an implementation.

How to optimize callbacks from a NSOperationQueue-started thread

Consider this:
#interface SomeViewController : UIViewController {
SomeChildObject *child;
}
#end
#implementation SomeViewController
- (void) viewDidLoad {
...
child.delegate = self;
}
- (void) somethingHappened {
NSInvocationOperation *operation = [[NSInvocationOperation alloc]
initWithTarget:child
selector:#selector(doSomething)
object:nil];
[someNsOperationQueue addOperation:operation];
[operation release];
}
- (void) callbackA:(SomeData *)someData {
[self performSelectorOnMainThread:#selector(callbackAonMainThread:)
withObject:someData
waitUntilDone:NO];
}
- (void) callbackAonMainThread:(SomeData *)someData {
... do something with results in main thread, e.g UI feedback
}
- (void) callbackB:(SomeData *)someData {
[self performSelectorOnMainThread:#selector(callbackBonMainThread:)
withObject:someData
waitUntilDone:NO];
}
- (void) callbackBonMainThread:(SomeData *)someData {
... do something with results in main thread, e.g UI feedback
}
#end
In English:
I have a view controller running in the main thread with a child model object to do something (fetch data over network). The view controller is the delegate for the child, so the child can signal the results back with delegation. To perform the expensive work, I spawn the child.doSomething method with a NSInvocationOperation that launches the operation in a background thread. When done, the child calls the delegate's (view controller's) callbackA or callbackB with some results. Since (I think) these callbacks are invoked in the same background thread where the doSomething call was run, I need to call performSelectorOnMainThread to transfer control back to main thread.
This works fine, but I do not like having two callback-related methods for each callback. (There are actually more callbacks, so the real code is even more bloated.) Ideally, I would do something like this:
- (void) callbackA:(SomeData *)someData {
if (not_running_on_main_thread) {
[self performSelectorOnMainThread:#selector(callbackA:)
withObject:someData
waitUntilDone:NO];
} else {
// now running on main thread, work with the results.
}
}
Questions:
1) how do I do the "not_running_on_main_thread" test?
2) is there any other way to cut down the callbacks bloat?
EDIT: ok, I never read NSThread docs before posting :) looks like [NSThread isMainThread] is what I am looking for. But is there any other way to restructure or make this nicer still?
Just check for [NSThread isMainThread]. There's nothing more you can do if you need multiple callbacks which do different things.
Only one thing I do differently, my code looks like this:
- (void) callbackA:(SomeData *)someData {
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(callbackA:)
withObject:someData
waitUntilDone:NO];
return;
}
// now running on main thread, work with the results.
}
This lets me get rid of the whole-function-long else and make the code a little clearer. And you can save on one indentation level this way ;-)