Asynchronous Requests (Cocoa)

Importance & Benefits

Asynchronous programming allows to resolve a lot of problems, making our applications more interactive and responsible, and smartly consumes system resources. But at the same time it requires certain efforts in the implementation and sometimes it complicates the whole project.

Support in DataAbstract

DataAbstract encourages using the asynchronous paradigm and simplifies implementing it. Each method of the DARemoteDataAdapter class contains several versions specially designed for calling it asynchronously. They usually have a begin* prefix. For example:

#pragma mark data access - asynchronous
- (DAAsyncRequest *) beginGetDataTable:(NSString *)tableName start:(BOOL)start;
...
#pragma mark data access - with blocks
- (DAAsyncRequest *) beginGetDataTable:(NSString *)tableName withBlock:(void (^)(DADataTable *))block;
...

As you can see, we are supporting two approaches here:

a) using delegates

b) using blocks

Using Delegates

The first approach is quite simple. We need to create an asynchronous request for the particular method. If we need to control the exact moment of starting the asynchronous request, we can specify NO as the start parameter, this will prevent the request from being started automatically. Usually we may want to delay the execution method until we configure the request properly. We may need to specify the delegate instance or/and provide some context for the request. We can use any instance of the class which implements the DAAsyncRequestDelegate protocol as delegate. This protocol exposes a set of the methods that can be called when the request is successfully completed asynchronous or when it fails. All methods of that protocol are optional, so you are free to implement only those you really need here.

Later, after we configured our request, we can run it using the start method, which will then run our request in a separate thread.

- (IBAction)loadData:(id)sender {
    
    NSArray *fields = [NSArray arrayWithObjects:
                       @"WorkerID", 
                       @"WorkerLastName", 
                       @"WorkerFirstName", 
                       @"WorkerPosition", nil];
    
    // compose asynchronous request but don't start it until we configure it properly
    DAAsyncRequest* request = [remoteDataAdapter beginGetDataTable:@"Workers" 
                                                                select:fields
                                                                 where:nil
                                                                 start:NO];

    // setting self as the delegate means that the current class should be able to process 
    // the result of the async call
    [request setDelegate:self];

    // we can also specify any data to pass to the result processing methods as the request context.
    [request setContext:@"beginGetDataTable request"];
    [request start];
}

When the async method ends its execution with success, it will try to find the asyncRequest:didReceiveTable: delegate method and invoke it with the appropriate parameters. All we need here is to consume the received data:

- (void)asyncRequest:(DAAsyncRequest *)request 
     didReceiveTable:(DADataTable *)table {

        // here we can obtain the request context we specified before, and use it for our own purposes
        id context = [request context];
        NSLog(@"Context is: %@", id);

        // consumes the data
    [self setWorkersTable:table];
    [tableView reloadData];
}

When the async method ends with a failure, it will try to find and invoke the asyncRequest:didFailWithException: delegate method. Here, we should add our logic for proper handling various exceptions:

- (void)  asyncRequest:(ROAsyncRequest *)request 
  didFailWithException:(NSException *)exception {

    [self showException:exception];
}

Using Blocks

Another approach is to use blocks. Blocks are a nice feature introduced in Mac OS X 10.6 Snow Leopard and iOS 4.0, which allows to specify and use anonymous methods. In this case, it doesn't require using a delegate and all the code can be specified in the same place as the blocks.

The DARemoteDataAdapter class has a set of overloaded methods for calling them with blocks. For example:

    
// compose and run the asynchronous request
[remoteDataAdapter beginGetDataTable:@"Workers" 
                              select:fields
                               where:nil
                           withBlock:^(DADataTable *table) {

                             // this code will be executed on the main thread when the request ends
                             [self setWorkersTable:table];
                             [tableView reloadData];
                           }];

When using blocks, you can also assign a second failureBlock to the asynchronous request, which will be called (in addition to the regular block) if the request fails:

// compose asynchronous request but don't start it until we configure it properly
DAAsyncRequest* request = [remoteDataAdapter beginGetDataTable:@"Workers" 
                                                        select:fields
                                                         where:nil
                                                         start:NO];
// add failure block
[request setFailureBlock:^(NSException *exception){
        
   // handle the exception 
   NSLog(@"%@", exception);                
}];

// run the request
[request startWithBlock:^{

    // this code will be executed on the main thread when the request ends
    [self setWorkersTable:table];
    [tableView reloadData];
}];

Tracking Asynchronous Requests

In the real application, where it is possible to run several asynchronous requests at the same time, it is nice to track the running request to notify the user that the application is working at the moment. This can be easily done, as shown below:

////////////////////////
// Header

@interface MainWindowController : NSWindowController {
  ...
  BOOL busy;
  NSMutableArray *requests;
  ...   
}
...
@property (assign) BOOL busy;
- (void)registerRequest:(ROAsyncRequest *)request;
- (void)unregisterRequest:(ROAsyncRequest *)request

@end

////////////////////////
// Implementation


@implementation MainWindowController

@synthesize busy;

- (void)registerRequest:(ROAsyncRequest *)request {
    
    [requests addObject:request];
    self.busy = YES;
}

- (void)unregisterRequest:(ROAsyncRequest *)request {
    
    [requests removeObject:request];
    self.busy = ([requests count] > 0);
}

- (IBAction)loadData:(id)sender {
   ...
   DAAsyncRequest* request = [remoteDataAdapter beginGetDataTable:@"Workers" 
                                                           select:fields
                                                            where:nil
                                                            start:NO];
   [request setContext:@"Downloading Workers data..."];
   [request setFailureBlock:^(NSException *exception){
        
      [self unRegisterRequest:request.request];
      NSLog(@"%@", exception);                
   }];

   [self registerRequest:request.request];
   [request startWithBlock:^{
      
      [self unRegisterRequest:request.request];
      [self setWorkersTable:table];
      [tableView reloadData];
   }];
}

@end