Working Asynchronously with the Remote Data Adapter

As noted in Working Synchronously or Asynchronously?, Data Abstract supports both Synchronous and Asynchronous programming styles for interacting between clients and the middle-tier.

While calling methods in a synchronous fashion is quite straight-forward and easier to do but it has one crucial drawback: low UI responsiveness. There will always be an unpredictable latency between the time you call the method and the time when you receive the result. The problem is that your users expect the UI to always be responsive and reflect their actions immediately.

This is where asynchronous support comes in, instead of hanging the UI you start off your call to the remote server and then return control to the main thread so that the UI remains fully responsive. Once the remote request completes, your code gets to take control again, and you can update your UI with the new data.

DARemoteDataAdapter was designed to support both synchronous and asynchronous calls. There are three approaches to organizing asynchronous requests. The first one is to use delegates which implement callback methods, the second is using blocks and the third is using Grand Central Dispatch.

Asynchronous calls using blocks

Blocks if you haven't used them before where introduced in Mac OS X Snow Leopard and iOS 4.0. Essentially, blocks allow you to define a section of code that will be executed at a later time, similar to anonymous methods or closures in other languages.

Blocks have full access to the surrounding scope, and thus provide a much more convenient way for defining callbacks than a delegate method or a separate function. For example, the following call to -getDataTable:withBlock: might be used to retrieve a table without blocking the calling thread:


// compose and run the asynchronous request
NSString *tableName = @"Customers";
[remoteDataAdapter beginGetDataTable:tableName
      withBlock:^(DADataTable * table) {

          // this code will be executed at the caller thread when request ends
          // "table" here contains the data retrieved from the server
          [self setCustomersTable:table];
          [tableView reloadData];

          // Here we will use variable tableName defined in the caller method,
          // outside the block
          NSLog(@"Received data for table %@", tableName);
}];

// Our main thread keeps running serving the user, while data is being fetched...

let tableName = "Customers"
self.remoteDataAdapter.beginGetDataTable(tableName) {
    (table:DADataTable!) -> Void in
        // this code will be executed at the caller thread when request ends
        // "table" here contains the data retrieved from the server

        setCustomersTable(table)
        self.tableView.reloadData()
        NSLog("Received data for table %@", table)
}

The other benefit of using blocks is that the code that will respond to your data request being completed, can be written within the same context as the original request, effectively making the asynchronous nature of the call more transparent and allowing you to see both parts of the code (what happens leading up to your data request, and what happens once the data is retrieved) in one single place.

Note: Code in a block will be executed on the caller thread. So if you start asynchronous request from the main thread then the block will execute in the main thread too. Thus you don't need to put in any extra effort to dispatch the block call on the main thread. This is very important when you need to perform some actions with your UI - which should only be done on the main thread.

You need to be careful, particularly when you call an asynchronous method from the background thread. In this case - the caller thread will be the background thread, and asynchronous block will be executed there. Thus if you will try to interact with your UI there then you will get in trouble.

Asynchronous calls using delegates

Another option available to you if you do not want to use blocks, is to instead use delegate callbacks to be informed when your data requests have been completed.

Your Delegate class should conform to the DARemoteDataAdapterDelegate protocol and implement some of callback methods which are called when the asynchronous request ends.

There are three delegate methods that are most relevant to this task:


- (void)asyncRequest:(DAAsyncRequest *)request didReceiveTable:(DADataTable *)table;
- (void)asyncRequest:(DAAsyncRequest *)request didReceiveTables:(NSDictionary *)tables;
- (void)asyncRequest:(ROAsyncRequest *)request didFailWithException:(NSException *)exception;

func asyncRequest(request: DAAsyncRequest!, didReceiveTable table: DADataTable!)
func asyncRequest(request: DAAsyncRequest!, didReceiveTables tables: [NSObject : AnyObject]!)
func asyncRequest(request: ROAsyncRequest!, didFailWithException exception: NSException!)

The first two methods are called on your delegate once the data request has completed. As the names indicate, asyncRequest:didReceiveTable: is called for each individual table - if you requested more than one table in one go, for example by calling beginGetDataTables:, this delegate method will be called multiple times.

In contrast, asyncRequest:didReceiveTables: will be called once per request, passing in a dictionary with all the tables you requested, keyed by the table name. In most cases, you would implement one, but not both of these methods, depending on how you prefer to handle the tables in your code.

For example, implementing asyncRequest:didReceiveTables: might make sense if you want to work with several tables at once, while asyncRequest:didReceiveTable: makes sense if you plan to perform independent actions for each table (you can check [table name] to distinguish which table you received with each call).

Callbacks order

If your delegate will have both of these methods implemented, and you are requesting several tables in one go then at the beginning multiple asyncRequest:didReceiveTable: callbacks will fire and finally asyncRequest:didReceiveTables: will be called.

The third method will be called if any error occurred during your request – for example the server might have been unreachable, or denied access to the table. You should make sure to always implement this method, otherwise you’d be wondering why your requests never seem to return and might give the user a false sense of the state of the data.

Remember you have to assign your delegate class directly to your DARemoteDataAdapter. We have found that this is quite a common mistake. If you don't, then none of the delegate methods will be called.

Note: Also you can manually assign a delegate to the DAAsyncRequest itself. If you do so, make sure to assign the delegate before starting your request to avoid concurrency issues and the highly unlikely event that your call completes before your assignment does.


...
// Somewhere in your code
NSArray *tableNames = [NSArray arrayWithObjects:@“Customers”, @“Orders”, nil];
DAAsyncRequest* request = [rda beginGetDataTables:tableNames
                                           select:nil
                                            where:nil
                                            start:NO];

[request setDelegate:self];
[request start];
...

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

- (void)asyncRequest:(ROAsyncRequest *)request
didFailWithException:(NSException *)exception {
  // Show error to the user
}

func asyncCall_UsingDAAsyncSwift() {
    let tableNames = ["Customers", "Orders"]

    let asyncRequest:DAAsyncRequest =
        self.remoteDataAdapter.beginGetDataTables(tableNames,
                      select: nil, `where`: nil, start: false)

    asyncRequest.delegate = self
    asyncRequest.start()
}

func asyncRequest(request: DAAsyncRequest!, didReceiveTable table: DADataTable!) {
    NSLog("Got the table %@", table)
}


func asyncRequest(request: ROAsyncRequest!, didFailWithException exception: NSException!) {
    //show error to user
    NSLog("Error Received %@", exception)
}

Note: Like for cases when we are using blocks, any of these callback methods are executed on the caller thread by default. Remember that you can safely interact with your UI from there.

Another useful delegate method you might want to implement is asyncRequest:didReceiveDataSize:ofExpected:, which will be called repeatedly as data is transferred from the server to the client. If you are downloading larger tables and expect transfers to take more than a few seconds, it allows you to present an accurate progress indicator to let the user know how the data retrieval is progressing. For example:


- (void)asyncRequest:(ROAsyncRequest *)request
  didReceiveDataSize:(int) size
          ofExpected:(int) totalSize {

  NSLog(@“Downloaded %d%%”, size*100/totalSize);
}

func asyncRequest(request: ROAsyncRequest!,
  didReceiveDataSize size: Int32,
     ofExpected totalSize: Int32) {

    NSLog("Downloaded %d%%", size*100/totalSize)
}

Asynchronous calls using Grand Central Dispatch (GCD)

The last option is to take advantage of Grand Central Dispatch, also commonly known as GCD, to perform actions in an asynchronous fashion. GCD, like Blocks, was added in Mac OS X 10.6 & iOS 4.0 and is a low-level C-based API that makes it simple to use a task-orientated concurrency model. It is an alternative option to using NSOperation/NSOperationQueue (which is actually built on top of GCD), though lacks some of the niceties like operation cancellation.

Instead of calling the asynchronous methods that DARemoteDataAdapter provides, you use dispatch_async to wrap around calls to the synchronous methods; for example getTable:. The problem with this approach is that should an error occur then an NSException will be thrown and will need to be handled, rather than an exception object (or NSError) being passed into a delegate method.

Handling exceptions in Objective-C is not an issue, however Swift does not have support for exception handling. (at least at the time of writing, Swift 2.0 due in the Fall of 2015 will have try/catch support that is intended for error's).


dispatch_queue_t globalQueue =
      dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(globalQueue, ^{
    DADataTable * customerTable = [self.dataAdapter getDataTable:@"Customers"];
    dispatch_async(dispatch_get_main_queue(), ^{
        //do something with the customerTable
        updateUIWithTable(customerTable);
    });
});


let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
    // retrieve the table named "Customers"
    let customerTable: DADataTable = self.dataAdapter.getDataTable("Customers")
    dispatch_async(dispatch_get_main_queue()) {
        // update the UI with the data
        self.updateUI(customerTable)
    }
}

Samples

Here are some examples of how to use DARemoteDataAdapter in an asynchronous way. We will highlight where applicable using blocks, delegates and grand central dispatch (commonly referred to as GCD).

Get Schema

Using blocks


-(void)beginDownloadSchema {
DAAsyncRequest* request =
  [remoteDataAdapter beginGetSchemaWithBlock:^(DASchema * schema) {
    NSLog(@"We've got a schema: %@", schema);
  }];

  [request setFailureBlock:^(NSException * ex) {
        NSLog(@"Something wrong happened here: %@", [ex reason]);
  }];
}

func beginDownloadSchema {
    let asyncRequest: DAAsyncRequest =
        self.remoteDataAdapter.beginGetSchemaWithBlock { (schema:DASchema!) -> Void in
        NSLog("We've got a schema: %@", schema)
    }

    asyncRequest.failureBlock = {(exception: NSException!) -> Void in
        NSLog("Something wrong happened here: %@", exception.reason!)
    }
}

Using delegates



-(void)beginDownloadSchema {
    DAAsyncRequest* request = [remoteDataAdapter beginGetSchema];
    [request setDelegate:self];
}

-(void)asyncRequest:(DAAsyncRequest *)request
   didReceiveSchema:(DASchema *)schema {
     NSLog(@"We've got a schema: %@", schema);
}

- (void)asyncRequest:(ROAsyncRequest *)request
didFailWithException:(NSException *)ex {
    NSLog(@"Something wrong happened here: %@", [ex reason]);
}

func beginDownloadSchemaWithDelegates() {
    let asyncRequest: DAAsyncRequest = self.remoteDataAdapter.beginGetSchema()
    asyncRequest.delegate = self
}

func asyncRequest(request: DAAsyncRequest!, didReceiveSchema schema: DASchema!) {
    NSLog("We've got a schema: %@", schema)
}

func asyncRequest(request: ROAsyncRequest!, didFailWithException exception: NSException!) {
    //show error to user
    NSLog("Something wrong happened here: %@", exception.reason!)
}

Get single table

Using blocks


-(void)beginDownloadWorker {

  DAAsyncRequest* request =
    [remoteDataAdapter beginGetDataTable:@"Worker"
                               withBlock:^(DADataTable * table) {
                                   NSLog(@"Received table %@", [table name]);
                               }];
}

func beginDownloadWorker() {
    let asyncRequest: DAAsyncRequest =
        self.remoteDataAdapter.beginGetDataTable("Worker") { (table: DADataTable!) -> Void in
        NSLog("Received table %@", table.name)
    }
}

Using delegates


-(void)beginDownloadWorker2 {
    DAAsyncRequest* request = [remoteDataAdapter
        beginGetDataTable:@"Worker" start:NO];
    [request setDelegate:self];
    [request start];
}

-(void)asyncRequest:(DAAsyncRequest *)request
    didReceiveTable:(DADataTable *)table {
    NSLog(@"We've got a table %@ (%d)", [table name], [table rowCount]);
}

- (void)asyncRequest:(ROAsyncRequest *)request
didFailWithException:(NSException *)ex {
    NSLog(@"Something wrong happened here: %@", [ex reason]);
}

func beginDownloadWorker() {
    let asyncRequest:DAAsyncRequest =
        self.remoteDataAdapter.beginGetDataTable("Worker", start: false)
    asyncRequest.delegate = self
    asyncRequest.start()
}

func asyncRequest(request: DAAsyncRequest!, didReceiveTable table: DADataTable!) {
    NSLog("We've got a table %@ (%d)", table.name, table.rowCount)
}

func asyncRequest(request: ROAsyncRequest!, didFailWithException exception: NSException!) {
    //show error to user
    NSLog("Something wrong happened here: %@", exception.reason!)
}

Get several tables in one go

Using blocks


-(void)beginDownloadTables {
  DAAsyncRequest* request =
    [rda beginGetDataTables:tableNames
                  withBlock:^(NSDictionary * tables) {
                      for (NSString * tableName in [tables allKeys]) {
                          DADataTable * table = [tables objectForKey:tableName];
                          NSLog(@"We've got a table %@ (%d)",
                                  [table name], [table rowCount]);
                      }
                  }];
}

func beginDownloadTables() {

    let tableNames = ["Customers", "Orders"]
    let asyncRequest: DAAsyncRequest =
            self.remoteDataAdapter.beginGetDataTables(tableNames) {
              (tables: [NSObject:AnyObject]!) -> Void in
        for (_, table) in tables {
            NSLog("We've got a table %@ (%d)", table.name, table.rowCount)
        }
    }
}

Using delegates


-(void)beginDownloadTables {

    NSArray * tableNames = [NSArray arrayWithObjects:
              @"Workers", "Customers", "Orders", nil];

    DAAsyncRequest* request = [rda beginGetDataTables:tableNames start:NO];
    [request setDelegate:self];
    [request start];
}

-(void)asyncRequest:(DAAsyncRequest *)request
    didReceiveTable:(DADataTable *)table {
    NSLog(@"We've got a table %@ (%d)", [table name], [table rowCount]);
}

-(void)asyncRequest:(DAAsyncRequest *)request
   didReceiveTables:(NSDictionary *)tables {

    NSLog(@"We've got some tables %@ (%ld)", [tables allKeys], [tables count]);

    for (NSString * tableName in [tables allKeys]) {
        DADataTable * table = [tables objectForKey:tableName];
        NSLog(@"We've got a table %@ (%d)", [table name], [table rowCount]);
    }
}

- (void)asyncRequest:(ROAsyncRequest *)request
didFailWithException:(NSException *)ex {
    NSLog(@"Something wrong happened here: %@", [ex reason]);
}

func beginDownloadTables() {
    let tableNames = ["Workers", "Customers", "Orders"]

    let asyncRequest:DAAsyncRequest =
      self.remoteDataAdapter.beginGetDataTables(tableNames, start: false)

    asyncRequest.delegate = self
    asyncRequest.start()
}

func asyncRequest(request: DAAsyncRequest!, didReceiveTable table: DADataTable!) {
    NSLog("We've got a table %@ (%d)", table.name, table.rowCount)
}

func asyncRequest(request: DAAsyncRequest!, didReceiveTables tables: [NSObject : AnyObject]!) {
    NSLog("We've got some tables:")

    for (_, table) in tables {
        NSLog("We got a table %@ (%d)", table.name, table.rowCount)
    }
}

func asyncRequest(request: ROAsyncRequest!, didFailWithException exception: NSException!) {
    //show error to user
    NSLog("Something wrong happened here: %@", exception.reason!)
}

Update Data

Using blocks


- (void) beginApplyUpdates {
    DAAsyncRequest * asyncRequest = [self.remoteDataAdapter beginApplyChangesForTable:dateTable withBlock:^{
        // some operation to refresh the UI
    }];
}


func beginApplyUpdates() {
    let _ : DAAsyncRequest = self.remoteDataAdapter.beginApplyChangesForTable(dataTable, withBlock: { () -> Void in
        // some operation to refresh the UI
    })
}

Using delegates


-(void)beginUpdateWorkers {
    DAAsyncRequest* request = [remoteDataAdapter
        beginApplyChangesForTable:workersTable start:NO];
    [request setDelegate:self];
    [request start];
}

-(void)asyncRequest:(DAAsyncRequest *)request
didFinishApplyingChangesForTables:(NSArray *)tables
                       withErrors:(NSArray *)errors {

    if (!errors) {
        NSLog(@"%li tables has been updated.", [tables count]);
        return;
    }

    NSLog(@"Some changes have not been applied: %@", errors);
}

-(void)asyncRequest:(DAAsyncRequest *)request
didFailToApplyChange:(DADeltaChange *)change
           forTable:(DADataTable *)table {

    NSLog(@"Change %@ has not be applied for table %@", change, table.name);
}

- (void)asyncRequest:(ROAsyncRequest *)request
didFailWithException:(NSException *)ex {
    NSLog(@"Something wrong happened here: %@", [ex reason]);
}

func beginUpdateWorkers() {
    let asyncRequest:DAAsyncRequest =
      self.remoteDataAdapter.beginApplyChangesForTable(workersTable, start: false)

    asyncRequest.delegate = self
    asyncRequest.start()
}

func asyncRequest(request: DAAsyncRequest!,
    didFinishApplyingChangesForTables tables: [AnyObject]!, withErrors errors: [AnyObject]!) {
    if (errors == nil) {
        NSLog("%li tables has been updated.", tables.count)
        return
    }

    NSLog("Some changes have not been applied: %@", errors)
}

func asyncRequest(request: DAAsyncRequest!,
      didFailToApplyChange change: DADeltaChange!, forTable table: DADataTable!) {
    NSLog("Change %@ has not be applied for table %@", change, table.name)
}

func asyncRequest(request: ROAsyncRequest!, didFailWithException exception: NSException!) {
    //show error to user
    NSLog("Something wrong happened here: %@", exception.reason!)
}

Call the Remote Command

Using blocks


-(void)beginCallCommandEx {

  NSDictionary * params =
    [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:10],
    @"groupId", nil];

  [dataAdapter beginExecuteCommandEx:@"TestStoredProcedure"
       withParameters:params
            withBlock:^(int result, NSDictionary * outParams) {
                NSLog(@"Called ExecuteCommandEx
                        for %@ with result %i", @"TestStoredProcedure", result);
                NSLog(@"%@", outParams);
}];

func beginCallCommandEx() {
    let params : [NSObject : AnyObject] = ["groupId" : 10]

    self.remoteDataAdapter.beginExecuteCommandEx("TestStoredProcedure",
      withParameters: params ) { (result:Int32, outParams: [NSObject : AnyObject]!) -> Void in
        NSLog("Called ExecuteCommandEx for %@ with result %i", "TestStoredProcedure", result)
        NSLog("%@", outParams)
    }
}

Using delegate


-(void)beginCallCommandEx {

  NSDictionary * params =
      [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:10],
      @"groupId", nil];

  DAAsyncRequest * request = [dataAdapter beginExecuteCommandEx:@"TestStoredProcedure"
                                                withParameters:params
                                                         start:NO];
  [request setDelegate:self];
  [request start];
}

     - (void)asyncRequest:(DAAsyncRequest *)request
didFinishExecutingCommand:(NSString *)commandName
               withResult:(int)result
          andOutputParams:(NSDictionary *)outParams
{
    NSLog(@"Called ExecuteCommandEx for %@ with result %i", commandName, result);
    NSLog(@"%@", outParams);
}

- (void)asyncRequest:(ROAsyncRequest *)request
didFailWithException:(NSException *)ex {
    NSLog(@"Something wrong happened here: %@", [ex reason]);
}

func beginCallCommandExDelegate() {
    let params : [NSObject : AnyObject] = ["groupId" : 10]

    let asyncRequest:DAAsyncRequest =
      self.remoteDataAdapter.beginExecuteCommandEx("TestStoredProcedure",
      withParameters: params, start: false)

    asyncRequest.delegate = self
    asyncRequest.start()
}

func asyncRequest(request: DAAsyncRequest!, didFinishExecutingCommand commandName: String!,
  withResult result: Int32, andOutputParams outParams: [NSObject : AnyObject]!) {
    NSLog("Called ExecuteCommandEx for %@ with result %i", "TestStoredProcedure", result)
    NSLog("%@", outParams)
}

func asyncRequest(request: ROAsyncRequest!, didFailWithException exception: NSException!) {
    //show error to user
    NSLog("Something wrong happened here: %@", exception.reason!)
}