Front-End Web & Mobile

The AWS Mobile SDK for iOS – How to use BFTask

We’ve released Version 2 of the AWS Mobile SDK for iOS with significant improvements to our previous SDK. One of the highlights is BFTask support. With native BFTask support in the SDK for iOS, you can chain async requests instead of nesting them. It makes the logic cleaner, while keeping the code more readable. In this blog post, I am going to show you some basics so that you can quickly get started with BFTask.

Asynchronous by default

Many methods in the v2 SDK return BFTask. It is important to remember that these methods are asynchronous methods. For example, AWSKinesisRecorder defines the following methods:

- (BFTask *)saveRecord:(NSData *)data
            streamName:(NSString *)streamName;

- (BFTask *)submitAllRecords;

These methods are asynchronous and return immediately. This means the following code snippet may not submit testData to the Amazon Kinesis stream:

AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder];

NSData *testData = [@"test-data" dataUsingEncoding:NSUTF8StringEncoding];
[kinesisRecorder saveRecord:testData
                 streamName:@"test-stream-name"];
[kinesisRecorder submitAllRecords];

This is because saveRecord:streamName: may return before it persists the record on the disk, and submitAllRecords does not see it on the disk. The correct way to submit the data is the following:

AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder];

NSData *testData = [@"test-data" dataUsingEncoding:NSUTF8StringEncoding];
[[[kinesisRecorder saveRecord:testData
                   streamName:@"test-stream-name"] continueWithSuccessBlock:^id(BFTask *task) {
    return [kinesisRecorder submitAllRecords];
}] continueWithBlock:^id(BFTask *task) {
    if (task.error) {
        NSLog(@"Error: %@", task.error);
    }
    return nil;
}];

Note that the submitAllRecords call is made in the continueWithSuccessBlock: block because you want to execute submitAllRecords after saveRecord:streamName: successfully finishes executing. continueWithBlock: and continueWithSuccessBlock: guarantee that when the block is executed, the previous asynchronous call has already finished executing. This is why you need to run submitAllRecords in the block.

continueWithBlock: vs. continueWithSuccessBlock:

continueWithBlock: and continueWithSuccessBlock: work in a similar way; making sure the previous asynchronous method finished executing when the block is executed. However, they have one important difference. continueWithSuccessBlock: will be skipped if an error occurred in the previous operation; on the other hand, continueWithBlock: is always executed.

In order to demonstrate the difference, let’s consider the following scenarios with the previous code snippet:

saveRecord:streamName: succeeded and submitAllRecords succeeded

  1. saveRecord:streamName: is successfully executed.
  2. continueWithSuccessBlock: is executed.
  3. submitAllRecords is successfully executed.
  4. continueWithBlock: is executed.
  5. Because task.error is nil, it does not log an error.
  6. Done.

saveRecord:streamName: succeeded and submitAllRecords failed

  1. saveRecord:streamName: is successfully executed.
  2. continueWithSuccessBlock: is executed.
  3. submitAllRecords is executed with an error.
  4. continueWithBlock: is executed.
  5. Because task.error is NOT nil, it logs an error from submitAllRecords.
  6. Done.

saveRecord:streamName: failed

  1. saveRecord:streamName: is executed with an error.
  2. continueWithSuccessBlock: is skipped and will NOT be executed.
  3. continueWithBlock: is executed.
  4. Because task.error is NOT nil, it logs an error from saveRecord:streamName:.
  5. Done.

Note that the above code snippet does not check for task.error in the continueWithSuccessBlock: block, and NSLog(@"Error: %@", task.error); may print out an error from either submitAllRecords or saveRecord:streamName:. This is a way to consolidate error handling logic at the end of the execution chain.

If you want to have each block deal with an error, you can rewrite the code snippet as follows:

AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder];

NSData *testData = [@"test-data" dataUsingEncoding:NSUTF8StringEncoding];
[[[kinesisRecorder saveRecord:testData
                   streamName:@"test-stream-name"] continueWithBlock:^id(BFTask *task) {
    if (task.error) {
        NSLog(@"Error from 'saveRecord:streamName:': %@", task.error);
        return nil;
    }
    return [kinesisRecorder submitAllRecords];
}] continueWithBlock:^id(BFTask *task) {
    if (task.error) {
        NSLog(@"Error from 'submitAllRecords': %@", task.error);
    }
    return nil;
}];

In this snippet, NSLog(@"Error from 'saveRecord:streamName:': %@", task.error); only prints out an error from saveRecord:streamName: and NSLog(@"Error from 'submitAllRecords': %@", task.error); prints out an error from submitAllRecords. By using continueWithBlock: and continueWithSuccessBlock: properly, you can flexibly control the error handling flow.

Always return BFTask or nil

In the above code snippet, it is returning nil at the end of continueWithBlock:, indicating successful execution of the block. Note that you are required to return either BFTask or nil in all of continueWithBlock: and continueWithSuccessBlock: blocks. In most cases Xcode warns you when you forget to do so; however, that is not always the case. If you forget to return BFTask or nil and Xcode does not catch it, it results in an app crash. Make sure you always return BFTask or nil.

Executing multiple tasks

If you want to execute a large number of operations, you have two options: executing in sequence and executing in parallel.

In sequence

If you want to submit 100 records to a Kinesis stream in sequence, you can accomplish it as follows:

AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder];

BFTask *task = [BFTask taskWithResult:nil];
for (int32_t i = 0; i < 100; i++) {
    task = [task continueWithSuccessBlock:^id(BFTask *task) {
        NSData *testData = [[NSString stringWithFormat:@"TestString-%02d", i] dataUsingEncoding:NSUTF8StringEncoding];
        return [kinesisRecorder saveRecord:testData
                                streamName:@"test-stream-name"];
    }];
}

[task continueWithSuccessBlock:^id(BFTask *task) {
    return [kinesisRecorder submitAllRecords];
}];

The key is to concatenate a series of tasks by reassigning task in this way: task = [task continueWithSuccessBlock:^id(BFTask *task) {.

In parallel

You can execute multiple methods in parallel by using taskForCompletionOfAllTasks: as follows:

AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder];

NSMutableArray *tasks = [NSMutableArray new];
for (int32_t i = 0; i < 100; i++) {
    NSData *testData = [[NSString stringWithFormat:@"TestString-%02d", i] dataUsingEncoding:NSUTF8StringEncoding];
    [tasks addObject:[kinesisRecorder saveRecord:testData
                                      streamName:@"test-stream-name"]];
}

[[BFTask taskForCompletionOfAllTasks:tasks] continueWithSuccessBlock:^id(BFTask *task) {
    return [kinesisRecorder submitAllRecords];
}];

You create an instance of NSMutableArray, put all of your tasks in it, and then pass it to taskForCompletionOfAllTasks:. taskForCompletionOfAllTasks: is successful only when all of the tasks are successfully executed. This approach may be faster, but may consume more system resources. Also, some AWS services such as Amazon DynamoDB throttle a large number of certain requests. Choose a sequential or parallel approach based on your use case.

Background thread vs. Main thread

continueWithBlock: and continueWithSuccessBlock: blocks are executed in the background thread by default. However, sometimes you need to execute certain operations on the main thread. One example is to update a UI component based on the result of a service call. There are two technologies that can help: Grand Central Dispatch and BFExecutor.

Grand Central Dispatch

You can use dispatch_async(dispatch_get_main_queue(), ^{...}); to execute a block on the main thread. In this example, you are creating an UIAlertView on the main thread when it failed to submit an record:

AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder];

NSData *testData = [@"test-data" dataUsingEncoding:NSUTF8StringEncoding];
[[[kinesisRecorder saveRecord:testData
                   streamName:@"test-stream-name"] continueWithSuccessBlock:^id(BFTask *task) {
    return [kinesisRecorder submitAllRecords];
}] continueWithBlock:^id(BFTask *task) {
    if (task.error) {
        dispatch_async(dispatch_get_main_queue(), ^{
            UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error!"
                                                                message:[NSString stringWithFormat:@"Error: %@", task.error]
                                                               delegate:nil
                                                      cancelButtonTitle:@"OK"
                                                      otherButtonTitles:nil];
            [alertView show];
        });
    }
    return nil;
}];

BFExecutor

Another option is to use BFExecutor:

AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder];

NSData *testData = [@"test-data" dataUsingEncoding:NSUTF8StringEncoding];
[[[kinesisRecorder saveRecord:testData
                   streamName:@"test-stream-name"] continueWithSuccessBlock:^id(BFTask *task) {
    return [kinesisRecorder submitAllRecords];
}] continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id(BFTask *task) {
    if (task.error) {
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error!"
                                                            message:[NSString stringWithFormat:@"Error: %@", task.error]
                                                           delegate:nil
                                                  cancelButtonTitle:@"OK"
                                                  otherButtonTitles:nil];
        [alertView show];
    }
    return nil;
}];

The entire withBlock: block is executed on the main thread in the above example.

Make it synchronous

When designing a new app, we recommend utilizing the asynchronous methods that the AWS Mobile SDK for iOS provides. However, sometime you already have apps that extensively use synchronous methods from v1 of the SDK and want to migrate them to use the v2 SDK with minimal effort. I will demonstrate how you can accomplish this.

In the beginning of this post, I explained why this code snippet may not work correctly:

AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder];

NSData *testData = [@"test-data" dataUsingEncoding:NSUTF8StringEncoding];
[kinesisRecorder saveRecord:testData
                 streamName:@"test-stream-name"];
[kinesisRecorder submitAllRecords];

This is because saveRecord:streamName: is an asynchronous operation, and when submitAllRecords is executed, saveRecord:streamName: may have not finished its execution. You can fix this by adding waitUntilFinished:

AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder];

NSData *testData = [@"test-data" dataUsingEncoding:NSUTF8StringEncoding];
[[kinesisRecorder saveRecord:testData
                  streamName:@"test-stream-name"] waitUntilFinished];
[kinesisRecorder submitAllRecords];

waitUntilFinished makes saveRecord:streamName: a synchronous operation, and when submitAllRecords is executed, saveRecord:streamName: has already finished executing. This is why this small change makes it a valid code snippet.

However, waitUntilFinished should be used cautiously. Never call waitUntilFinished on the main thread. It may block the main thread, and your app becomes sluggish and may be killed by the OS.


Once you are more comfortable with BFTask, the README on the GitHub repo of Bolts has in-depth documentation. Bolts is well-supported by other large companies such as Facebook and is actively being updated. We believe Bolts is currently the best solution to offer our developers the best development experience comparing to some other new JavaScript Promise-like frameworks such as PromiseKit, which is still in its pre-release stage.

I hope this helps you get started with the new AWS Mobile SDK for iOS.