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
-
saveRecord:streamName:
is successfully executed. -
continueWithSuccessBlock:
is executed. -
submitAllRecords
is successfully executed. -
continueWithBlock:
is executed. - Because
task.error
isnil
, it does not log an error. - Done.
saveRecord:streamName:
succeeded and submitAllRecords
failed
-
saveRecord:streamName:
is successfully executed. -
continueWithSuccessBlock:
is executed. -
submitAllRecords
is executed with an error. -
continueWithBlock:
is executed. - Because
task.error
is NOTnil
, it logs an error fromsubmitAllRecords
. - Done.
saveRecord:streamName:
failed
-
saveRecord:streamName:
is executed with an error. -
continueWithSuccessBlock:
is skipped and will NOT be executed. -
continueWithBlock:
is executed. - Because
task.error
is NOTnil
, it logs an error fromsaveRecord:streamName:
. - 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.