views:

59

answers:

2

I have a library project that uses ASIHTTPRequest to make URL requests and parse the responses. The library will be used by a separate iPhone app project.

If my iPhone controller code responds to a touch event, then calls into the library to make URL requests, how do I best perform the requests asynchronously?

In the library, if I use the delegate pattern for asynchronous requests as shown in the ASIHTTPRequest sample code, how do I return data from the library back to the calling code in the iPhone controller?

If I instead make synchronous URL requests with ASIHTTPRequest inside the library, what's the easiest way to put the calls to the library from the iPhone controller on a separate thread to avoid tying up the UI thread?

+1  A: 

I'm no ASIHTTPRequest expert (NSURLRequest has always done me fine), but from a quick poke at the code, it looks like you'd use its delegate and didFinishSelector properties to give it someone to tell when the URL request is finished. So, for example:

- (void)startURLRequest
{
    ASIHTTPRequest *myRequest;

    /* code to set the request up with your target URL, etc here */

    myRequest.delegate = self;
    myRequest.didFinishSelector = @selector(HTTPRequestDidFinish:);

    /* ... */

    [myRequest startAsynchronous];
}

- (void)HTTPRequestDidFinish:(ASIHTTPRequest *)request
{
    NSLog(@"Request %@ did finish, got data: %@", request, request.data);
    [myTargetForData didReceiveData:request.data fromURL:request.originalURL];
}

Apple explicitly recommend that you use the built-in runloop style mechanisms for asynchronous HTTP fetching, not separate threads. Using separate threads is likely to result in worse performance — at least in terms of battery life and/or device heat, even if it's still fast enough.

That said, as a learning point, by far the quickest way to switch something onto a separate thread and have it report back to the main thread (remember: UIKit objects may be messaged only from the main thread) is by changing this:

- (void)postResult:(NSString *)result
{
    instanceOfUILabel.text = result;
}

- (void)doExpensiveOperationOn:(NSString *)source
{
     /* lots of expensive processing here, and then... */
     [self postResult:result];
}

- (IBAction)userWantsOperationDone:(id)sender
{
     [self doExpensiveOperationOn:@"some value or another"];
}

Into this:

- (void)postResult:(NSString *)result
{
    instanceOfUILabel.text = result;
}

- (void)doExpensiveOperationOn:(NSString *)source
{
     /* we're on a thread without an autorelease pool now, probably we'll want one */
     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

     /* lots of expensive processing here, and then... */

     /* in this simplified example, we assume that ownership of 'result' is here on this thread, possibly on the autorelease pool, so wait until postResult has definitely finished before doing anything that might release result */
     [self performSelectorOnMainThread:@selector(postResult:) withObject:result waitUntilDone:YES];

     [pool release];
}

- (IBAction)userWantsOperationDone:(id)sender
{
     [self performSelectorOnBackgroundThread:@selector(doExpensiveOperationOn:) withObject:@"some value or another"];
}

There's about a million possible concurrency errors you can make by just going threaded without thinking about it though, and in that example an obvious problem is that whatever triggered the IBAction can [probably] trigger it several more times before doExpensiveOperationOn has finished. Multithreading is not something to be dashed into lightly.

Tommy
Thanks, I know about the delegates in ASIHTTPReuquest, but what's the best way to pass information from them back to calling code?In your example, the code for ASIHTTPRequest is inside a library. The object myTargetForData is a calling object outside the library. So in order to call its methods, I would have to pass in a reference to the calling object.Is that really the cleanest way to do it, and what is intended with the delegate pattern?
nekno
I obviously gave an entirely unhelpful answer then! In my case, I usually find that the class I have posting off HTTP requests knows what logic to apply to the result to get something I can use at runtime, so I tend to process the result in there into whatever class I'm using to encapsulate the result and then pass that on to the next delegate. So if I have a JSON or XML parse then it's connected to a one shot class that is the delegate of NSURLRequest (or ASIHTTPRequest in your case) and then the results go back to whatever my actual model class is. Specifics beyond that depend on the task.
Tommy
A: 

For anyone's future reference, the easiest approach I found is to use the async request functionality built into ASIHTTPRequest, setting my library object as the delegate and setting the didFinishSelector: and didFailSelector: values to different methods inside my library for each request.

At the end of processing each response, I assign the parsed response (an NSString* or NSArray*) to a property of my library object instead of returning a value.

When my iOS view controller delegate is loaded, I add a change observer to each of the properties in the library using Key-Value Observing. When the response is parsed and assigned to the property in the library, the observeValueForKeyPath:ofObject:change:context: method is called in the code of my view controller delegate, and from there I can figure out which property was changed and therefore what UI needs to be updated.

nekno