Blocks is Objective C's take on closures, which are commonly used in JavaScript AJAX code as callbacks for processing the response to a network request. For example, when using jQuery.ajax(), you'd often specify the callback that processes the AJAX response as a closure that's in-lined with the rest of the code that dispatches the network request. The benefit of this syntax is that your request and response code is kept close to each other and can very conveniently access the same set of data and variables.
// One of the reasons people love JavaScript
$.ajax({
url: 'ajax/my_handler',
success: function(data) {
// Do something useful with data
alert('Success!');
}
});Pre-iOS 4/Blocks, to accomplish something similar, you'll have to use a delegate object and specify the selectors to call when the network request has finished. Things get tricky when you need to pass data from the method dispatching the request to the method handling the response. You'd either have to set up a whole bunch of instance variables to persist the data, or package everything up in an NSDictionary that travels with the request object. Many novice iOS developers cheat by simply ignoring the whole async problem and doing all the network calls synchronously, which blocks the main UI thread and makes the UI unresponsive while the user waits for the network request to finish. Not a good idea.
With Blocks and her partner in crime, Grand Central Dispatch (GCD), it is now possible to structure your Objective C code like an AJAX webapp, which dramatically cuts down the amount of glue code you need to write to integrate with HTTP APIs. Blocks and GCD are fairly complex topics in their own right and Apple has published several must-read docs on these topics (e.g. this, that), but the good news is that you don't need to know a whole lot about them to start using them.
For starters, what I've found to work quite well is to package all my networking code in a single class (e.g. RPCService.h) and expose methods that take completion handler blocks (i.e. callbacks). That way, the rest of my app doesn't have to worry (or know) about the server-client communication that happens under the hood. This approach also simplifies offline testing: you can easily swap out your service object with a mock object without having to touch the rest of your code.
The RPCService.h methods typically take the following form:
- (void)doSomethingInterestingOnServerWith:(id)object
completion:(void (^)(NSError *error, id results));To call the methods, you'd write:
NSString *random = [NSString stringWithFormat:@"Hello %@", @"World"];
[rpcService doSomethingInterestingOnServerWith:object completion:^(NSError *error, id results) {
// Do something interesting with results.
NSLog(@"Some random text: %@", random); // random is available here!
}];Notice in the example above that the code in the block has access to the variables that are in the enclosing scope. There's a bunch of rules that you need to be aware of when you do so but the Cliff's Notes summary is that stack variables as accessible like const variables (you can read but not modify them) and Objective C objects (e.g. random in the example) are automatically retained and released by the block.
So what does the implementation of doSomethingOnServerWith:completion: look like?
- (void)doSomethingInterestingOnServerWith:(id)object
completion:(void (^)(NSError *error, id results)) {
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
completion = [completion copy];
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(defaultQueue, ^{
// Stuff below happens on a background thread
NSURL *url = // Your HTTP request handler URL here
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request addPostValue:value1 forKey:param1];
[request addPostValue:value2 forKey:param1];
// Do network request synchronously, in the background
[request startSynchronous];
// Do something with the request response here.
dispatch_async(dispatch_get_main_queue(), ^{
// Stuff below happens on the main thread
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
// Do additional things here that must be done on the main UI thread.
completion(nil, /* results */); // Error handling skipped for simplicity
[completion release];
});
});
}If you are seeing GCD dispatch_* calls for the first time, I apologize for how ugly they look. Yes, GCD is a low-level C API, enough said.
The key things to note about the code snippet:
- You should invoke copy on the block when you first receive it, and release it when you are done calling it. (Note that blocks behave like Objective C objects in the sense that you can call copy/retain/release on them and pass them around like any Objective object.) The reason for doing so is that blocks start out on the stack and need to be moved to the heap via copy so that they will still be valid when you are ready to execute them.
- There's a block nested within a block here. The outer block is the part that is executed in the background, dispatched to the DISPATCH_QUEUE_PRIORITY_DEFAULT concurrent queue (processes multiple blocks concurrently) via the first dispatch_async call. The inner block is executed on the main thread, via a subsequent dispatch to the main queue (obtained using dispatch_get_main_queue()). The main queue is a serial queue, and you'd use it the way you'd schedule tasks on the main run loop (think performSelector:withObject:afterDelay:).
- The network operation itself is a blocking, synchronous call. In the example, I used ASIFormDataRequest (from the most excellent ASIHttpRequest open-source library) to send the network request but you can of course use any HTTP framework you like, including good old NSURLConnection.
- Don't forget to use the network activity spinner to let the user know that something is going on over the network. It's a nice touch that users will appreciate.
- I call the completion handler on the main thread as a convenience so that I don't have to worry too much about thread safety in the completion handler. Your typical completion handler will use the response to update the UI, so you'll want the handler to be called on the main thread anyway as some UIKit calls are not thread safe.
Happy AJAX-style RPC-ing in Objective C!
No comments:
Post a Comment