To work with asynchronous operations without blocking the UI thread, the SDK provides two options:
completionHandler
, a streamlined class which offers a simple, common pattern for most API calls
and
AWSTask
, a class which is a renamed version of BFTask from the Bolts framework. AWSTasks gives advantages for more complex operations like chaining asynchronous requests.For complete documentation on Bolts, see the Bolts-ObjC repo.
Most simple asynchronous API method calls can use completionHandler
to handle method callbacks. When an asynchronous method is complete, completionHandler
returns two parts: a response object containing the method's return if the call was successful, or nil
if failed; and an error object containing the NSError
state when a call fails, or nil
upon success.
The following code shows typical usage of completionHandler
using Amazon Kinesis Firehose as the example.
- iOS - Swift
var firehose = AWSFirehose.default() firehose.putRecord(AWSFirehosePutRecordInput(), completionHandler: {(_ response: AWSFirehosePutRecordOutput?, _ error: Error?) -> Void in if error != nil { //handle error } else { //handle response } })- iOS - Objective-C
AWSFirehose *firehose = [AWSFirehose defaultFirehose]; [firehose putRecord:[AWSFirehosePutRecordInput new] completionHandler:^(AWSFirehosePutRecordOutput* _Nullable response, NSError * _Nullable error) { if(error){ //handle error }else{ //handle response } }];
An AWSTask
object represents the result of an asynchronous method. Using AWSTask
, you can wait for an asynchronous method to return a value, and then something with that value after it has returned. You can chain asynchronous requests instead of nesting them. This helps keep logic clean and code readable.
The following code shows how to use continueOnSuccessBlockWith:
and continueWith:
to handle methods calls that return an AWSTask
object.
- iOS - Swift
let kinesisRecorder = AWSKinesisRecorder.default() let testData = "test-data".data(using: .utf8) kinesisRecorder?.saveRecord(testData, streamName: "test-stream-name").continueOnSuccessWith(block: { (task:AWSTask<AnyObject>) -> AWSTask<AnyObject>? in // Guaranteed to happen after saveRecord has executed and completed successfully. return kinesisRecorder?.submitAllRecords() }).continueWith(block: { (task:AWSTask<AnyObject>) -> Any? in if let error = task.error as? NSError { print("Error: \(error)") return nil } return nil })- iOS - Objective-C
AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder]; NSData *testData = [@"test-data" dataUsingEncoding:NSUTF8StringEncoding]; [[[kinesisRecorder saveRecord:testData streamName:@"test-stream-name"] continueWithSuccessBlock:^id(AWSTask *task) { return [kinesisRecorder submitAllRecords]; }] continueWithBlock:^id(AWSTask *task) { if (task.error) { NSLog(@"Error: %@", task.error); } return nil; }];
The submitAllRecords
call is made within the continueOnSuccessWith
/ continueWithSuccessBlock:
because we want to run submitAllRecords
after saveRecord:streamName:
successfully finishes running. The continueWith
and continueOnSuccessWith
won't run until the previous asynchronous call finishes. In this example, submitAllRecords
is guaranteed to see the result of saveRecord:streamName:
.
The continueWith:
and continueOnSuccessWith:
block calls work in similar ways. Both ensure that the previous asynchronous method finishes executing before the subsequent block runs. However, they have one important difference: continueOnSuccessWith:
is skipped if an error occurred in the previous operation, but continueWith:
is always executed.
For example, consider the following scenarios, which refer to the preceding code snippet above.
saveRecord:streamName:
succeeded andsubmitAllRecords
succeeded.In this scenario, the program flow proceeds as follows:
saveRecord:streamName:
is successfully executed.continueOnSuccessWith:
is executed.submitAllRecords
is successfully executed.continueWith:
is executed.- Because
task.error
is nil, it doesn't log an error.- Done.
saveRecord:streamName:
succeeded andsubmitAllRecords
failed.In this scenario, the program flow proceeds as follows:
saveRecord:streamName:
is successfully executed.continueOnSuccessWith
is executed.submitAllRecords
is executed with an error.continueWithBlock:
is executed.- Because
task.error
is NOT nil, it logs an error fromsubmitAllRecords
.- Done.
saveRecord:streamName:
failed.In this scenario, the program flow proceeds as follows:
saveRecord:streamName:
is executed with an error.continueOnSuccessWith:
is skipped and will NOT be executed.continueWithBlock:
is executed.- Because
task.error
is NOT nil, it logs an error fromsaveRecord:streamName:
.- Done.
The preceding example consolidates error handling logic at the end of the execution chain for both methods called. It doesn't check for task.error
in continueOnSuccessBlockWith:
, but waits until the continueWith:
block executes to do so. An error from either the submitAllRecords
or the saveRecord:streamName:
method will be printed.
The following code shows how to implement the same behavior, but makes error handling specific to each method. submitAllRecords
is only called if saveRecord:streamName
succeeds, however, in this case, the saveRecord:streamName
call uses continueWith:
, the block logic checks task.error
and returns nil upon error. If that block succeeds then submitAllRecords
is called using continueWith:
in a block that also checks task.error
for its own context.
- iOS - Swift
let kinesisRecorder = AWSKinesisRecorder.default() let testData = "test-data".data(using: .utf8) kinesisRecorder?.saveRecord(testData, streamName: "test-stream-name").continueWith(block: { (task:AWSTask<AnyObject>) -> AWSTask<AnyObject>? in if let error = task.error as? NSError { print("Error from 'saveRecord:streamName:': \(error)") return nil } return kinesisRecorder?.submitAllRecords() }).continueWith(block: { (task:AWSTask<AnyObject>) -> Any? in if let error = task.error as? NSError { print("Error from 'submitAllRecords': \(error)") return nil } return nil })- iOS - Objective-C
AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder]; NSData *testData = [@"test-data" dataUsingEncoding:NSUTF8StringEncoding]; [[[kinesisRecorder saveRecord:testData streamName:@"test-stream-name"] continueWithBlock:^id(AWSTask *task) { if (task.error) { NSLog(@"Error from 'saveRecord:streamName:': %@", task.error); return nil; } return [kinesisRecorder submitAllRecords]; }]continueWithBlock:^id(AWSTask *task) { if (task.error) { NSLog(@"Error from 'submitAllRecords': %@", task.error); } return nil; }];
Remember to return either an AWSTask
object or nil
in every usage of continueWith:
and continueOnSuccessWith:
. In most cases, Xcode provides a warning if there is no valid return present, but in some cases an undefined error can occur.
If you want to execute a large number of operations, you have two options: executing in sequence or executing in parallel.
You can submit 100 records to an Amazon Kinesis stream in sequence as follows:
- iOS - Swift
var task = AWSTask<AnyObject>(result: nil) for i in 0...100 { task = task.continueOnSuccessWith(block: { (task:AWSTask<AnyObject>) -> AWSTask<AnyObject>? in return kinesisRecorder!.saveRecord(String(format: "TestString-%02d", i).data(using: .utf8), streamName: "YourStreamName") }) } task.continueOnSuccessWith { (task:AWSTask<AnyObject>) -> AWSTask<AnyObject>? in return kinesisRecorder?.submitAllRecords() }- iOS - Objective-C
AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder]; AWSTask *task = [AWSTask taskWithResult:nil]; for (int32_t i = 0; i < 100; i++) { task = [task continueWithSuccessBlock:^id(AWSTask *task) { NSData *testData = [[NSString stringWithFormat:@"TestString-%02d", i] dataUsingEncoding:NSUTF8StringEncoding]; return [kinesisRecorder saveRecord:testData streamName:@"test-stream-name"]; }]; } [task continueWithSuccessBlock:^id(AWSTask *task) { return [kinesisRecorder submitAllRecords]; }];
In this case, the key is to concatenate a series of tasks by reassigning task
.
- iOS - Swift
task.continueOnSuccessWith { (task:AWSTask<AnyObject>) -> AWSTask<AnyObject>? in- iOS - Objective-C
task = [task continueWithSuccessBlock:^id(AWSTask *task) {
You can execute multiple methods in parallel by using taskForCompletionOfAllTasks:
as follows.
- iOS - Swift
var tasks = Array<AWSTask<AnyObject>>() for i in 0...100 { tasks.append(kinesisRecorder!.saveRecord(String(format: "TestString-%02d", i).data(using: .utf8), streamName: "YourStreamName")!) } AWSTask(forCompletionOfAllTasks: tasks).continueOnSuccessWith(block: { (task:AWSTask<AnyObject>) -> AWSTask<AnyObject>? in return kinesisRecorder?.submitAllRecords() }).continueWith(block: { (task:AWSTask<AnyObject>) -> Any? in if let error = task.error as? NSError { print("Error: \(error)") return nil } return nil })- iOS - Objective-C
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"]]; } [[AWSTask taskForCompletionOfAllTasks:tasks] continueWithSuccessBlock:^id(AWSTask *task) { return [kinesisRecorder submitAllRecords]; }];
In this example you create an instance of NSMutableArray
, put all of our tasks in it, and then pass it to taskForCompletionOfAllTasks:
, which is successful only when all of the tasks are successfully executed. This approach may be faster, but it 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.
By default, continueWithBlock:
and continueWithSuccessBlock:
are executed on a background thread. But in some cases (for example, updating a UI component based on the result of a service call), you need to execute an operation on the main thread. To execute an operation on the main thread, you can use Grand Central Dispatch or AWSExecutor
.
The following example shows the use of dispatch_async(dispatch_get_main_queue(), ^{...});
to execute a block on the main thread. For error handling, it creates a UIAlertView
on the main thread when record submission fails.
- iOS - Swift
let kinesisRecorder = AWSKinesisRecorder.default() let testData = "test-data".data(using: .utf8) kinesisRecorder?.saveRecord(testData, streamName: "test-stream-name").continueOnSuccessWith(block: { (task:AWSTask<AnyObject>) -> AWSTask<AnyObject>? in return kinesisRecorder?.submitAllRecords() }).continueWith(block: { (task:AWSTask<AnyObject>) -> Any? in if let error = task.error as? NSError { DispatchQueue.main.async(execute: { let alertController = UIAlertView(title: "Error!", message: error.description, delegate: nil, cancelButtonTitle: "OK") alertController.show() }) return nil } return nil })- iOS - Objective-C
AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder]; NSData *testData = [@"test-data" dataUsingEncoding:NSUTF8StringEncoding]; [[[kinesisRecorder saveRecord:testData streamName:@"test-stream-name"] continueWithSuccessBlock:^id(AWSTask *task) { return [kinesisRecorder submitAllRecords]; }] continueWithBlock:^id(AWSTask *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; }];
Another option is to use AWSExecutor
as follows.
- iOS - Swift
let kinesisRecorder = AWSKinesisRecorder.default() let testData = "test-data".data(using: .utf8) kinesisRecorder?.saveRecord(testData, streamName: "test-stream-name").continueOnSuccessWith(block: { (task:AWSTask<AnyObject>) -> AWSTask<AnyObject>? in return kinesisRecorder?.submitAllRecords() }).continueWith(executor: AWSExecutor.mainThread(), block: { (task:AWSTask<AnyObject>) -> Any? in if let error = task.error as? NSError { let alertController = UIAlertView(title: "Error!", message: error.description, delegate: nil, cancelButtonTitle: "OK") alertController.show() return nil } return nil })- iOS - Objective-C
AWSKinesisRecorder *kinesisRecorder = [AWSKinesisRecorder defaultKinesisRecorder]; NSData *testData = [@"test-data" dataUsingEncoding:NSUTF8StringEncoding]; [[[kinesisRecorder saveRecord:testData streamName:@"test-stream-name"] continueWithSuccessBlock:^id(AWSTask *task) { return [kinesisRecorder submitAllRecords]; }] continueWithExecutor:[AWSExecutor mainThreadExecutor] withBlock:^id(AWSTask *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; }];
In this case, withBlock:
(Objective-C) or block:
(Swift) is executed on the main thread.