This guide provides an overview of Promises framework for Swift and Objective-C.
Typically, async operations take a completion handler in a form of a block, which is called to provide either a result or an error. To perform more than one async operation, you have to nest the second one inside the completion block of the first one, and also handle an error gracefully. Often such nesting becomes painful to follow or modify:
Objective-C:
- (void)getCurrentUserContactsAvatars:(void (^)(NSArray<UIImage *> *, NSError *))completion {
[MyClient getCurrentUserWithCompletion:^(MyUser *currentUser, NSError *error) {
if (error) {
completion(nil, error);
return;
}
[MyClient getContactsForUser:currentUser
completion:^(NSArray<MyContact *> *contacts, NSError *error) {
if (error) {
completion(nil, error);
return;
}
if (contacts.count == 0) {
completion(@[], nil);
return;
}
NSMutableArray<UIImage *> *avatars = [NSMutableArray array];
NSUInteger __block count = contacts.count;
BOOL __block errorReported = NO;
for (NSUInteger index = 0; index < count; ++index) {
[avatars addObject:[NSNull null]];
}
[contacts enumerateObjectsUsingBlock:^(MyContact *contact, NSUInteger index, BOOL __unused *_) {
[MyClient getAvatarForContact:contact completion:^(UIImage *avatar, NSError *error) {
if (errorReported) {
return;
}
if (error) {
completion(nil, error);
errorReported = YES;
return;
}
if (avatar) {
avatars[index] = avatar;
}
if (--count == 0) {
completion(avatars, nil);
}
}];
}];
}];
}];
}
Swift:
func getCurrentUserContactsAvatars(_ completion: ([UIImage]?, Error?) -> Void) {
MyClient.getCurrentUser() { currentUser, error in
guard error == nil else {
completion(nil, error)
return
}
MyClient.getContacts(currentUser) { contacts, error in
guard error == nil else {
completion(nil, error)
return
}
guard let contacts = contacts, !contacts.isEmpty() else {
completion([UIImage](), nil)
return
}
var count = contacts.count
var avatars = [UIImage](repeating: nil, count: count)
var errorReported = false
for (index, contact) in contacts.enumerated() {
MyClient.getAvatar(contact) { avatar, error in
if (errorReported) {
return
}
guard error == nil {
completion(nil, error)
errorReported = true
return
}
if let avatar = avatar {
avatars[index] = avatar
}
count -= 1
if count == 0 {
completion(avatars.flatMap { $0 }, nil)
}
}
}
}
}
}
Which could be used as:
Objective-C:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self getCurrentUserContactsAvatars:^(NSArray<UIImage *> *avatars, NSError *error) {
if (error) {
[self showErrorAlert:error];
} else {
[self updateAvatars:avatars];
}
}];
}
Swift:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
getCurrentUserContactsAvatars() { avatars, error in
if (error) {
showErrorAlert(error)
} else {
updateAvatars(avatars)
}
}
}
The code sample above, when converted into promises, could look like the
following (assuming you've got
-fbl_map
method on NSArray
):
Objective-C:
- (FBLPromise<NSArray<UIImage *> *> *)getCurrentUserContactsAvatars {
return [[[MyClient getCurrentUser] then:^id(MyUser *currentUser) {
return [MyClient getContactsForUser:currentUser];
}] then:^id(NSArray<MyContact *> *contacts) {
return [FBLPromise all:[contacts fbl_map:^id(MyContact *contact) {
return [MyClient getAvatarForContact:contact];
}]];
}];
Swift:
func getCurrentUserContactsAvatars() -> Promise<[UIImage]> {
return MyClient.getCurrentUser().then(MyClient.getContacts).then { contacts in
all(contacts.map(MyClient.getAvatar))
}
}
That's all!
Now use it like:
Objective-C:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[[[self getCurrentUserContactsAvatars] then:^id(NSArray<UIImage *> *avatars) {
[self updateAvatars:avatars];
return avatars;
}] catch:^(NSError *error) {
[self showErrorAlert:error];
}];
}
Swift:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
getCurrentUserContactsAvatars().then(updateAvatars).catch(showErrorAlert)
}
In general, a promise represents the eventual result of an asynchronous task, respectively the error reason when the task fails. Similar concepts are also called futures (see also wiki article: Futures and promises).
A promise can be in one of three states:
- pending - the promise is unresolved and the result is not yet available
- fulfilled - the promise is resolved with some value
- rejected - the promise is resolved with some error
Once fulfilled or rejected, a promise can never change its state in the future. Also, it can have an infinite number of observers waiting for it to be resolved. Once resolved, either a value or an error is broadcasted to all observers. Each observer, returns a new promise on subscribe, which, in turn, will be resolved with another value or error the observer provides. This enables chaining promises together to create a pipeline into a pipeline of transforming values which are computed asynchronously on different threads.
Thus, promises are a way of formalizing completion handlers to make chaining async tasks much easier. For example, it becomes trivial to write reusable code that can:
- perform a chain of dependent asynchronous operations with one completion block at the end
- have a fall-through behavior for errors to the nearest error handler
- perform many independent asynchronous operations simultaneously with one completion block
- race many asynchronous operations and return the value of the first to complete
- retry asynchronous operations
- and much more
Promises is a modern framework that implements the aforementioned synchronization construct in Swift and Objective-C.
- Simple: The framework has intuitive APIs that are well documented making it painless to integrate into new or existing code.
- Interoperable: Supports both Objective-C and Swift. Promises that are created in Objective-C can be used in Swift and vice versa.
- Lightweight: Has minimum overhead that achieves similar performance to GCD and completion handlers.
- Flexible: Observer blocks can be dispatched on any thread or custom queue.
- Safe: All promises and observer blocks are captured by GCD which helps avoid potential retain cycles.
- Tested: The framework has 100% test coverage.
One of the biggest concerns for all frameworks is the overhead they add on top of the standard library (GCD in this case).
The data below was collected by running performance tests for Objective-C and Swift on an iPhone 6s iOS 11.2.1 for the popular frameworks: PromiseKit, BrightFutures, Hydra, RxSwift and plain GCD for comparison.
-
Sizes in bytes added to a binary linked with each library in release mode:
Framework Objective-C Swift Promises 74160 79280 PromiseKit 393036 309248 BrightFutures N/A 83424 Hydra N/A 111600 RxSwift N/A 191680 -
Average time in seconds needed to create a resolved promise and get into a chained block on a serial queue (measured with 10,000 tries):
Framework Objective-C Swift GCD 0.000022744 0.000021246 Promises 0.000028293 0.000038103 PromiseKit 0.000047047 0.000038818 BrightFutures N/A 0.000038729 Hydra N/A 0.000061406 RxSwift N/A 0.000053124 -
Average time in seconds needed to create a resolved promise, chain 2 blocks and get into the last chained block on a serial queue (measured with 10,000 tries):
Framework Objective-C Swift GCD 0.000023336 0.000024622 Promises 0.000035536 0.000048412 PromiseKit 0.000071271 0.000061765 BrightFutures N/A 0.000044416 Hydra N/A 0.000086497 RxSwift N/A 0.000060675 -
Average time in seconds needed to create a resolved promise, chain 3 blocks and get into the last chained block on a serial queue (measured with 10,000 tries):
Framework Objective-C Swift GCD 0.000024501 0.000025683 Promises 0.000039605 0.000053961 PromiseKit 0.000088739 0.000079487 BrightFutures N/A 0.000049025 Hydra N/A 0.000108063 RxSwift N/A 0.00006914 -
Total time in seconds needed to resolve 10,000 pending promises with chained blocks and wait for control to get into each block on a concurrent queue:
Framework Objective-C Swift GCD 0.004818658 0.014513761 Promises 0.020536681 0.041234746 PromiseKit 0.074951688 0.067808994 BrightFutures N/A 0.083329189 RxSwift N/A 0.160872425
In your BUILD
file, add Promises
deps to corresponding targets:
objc_library(
# ...
deps = [
"//path/to/Promises:FBLPromises",
],
# ...
)
swift_library(
# ...
deps = [
"//path/to/Promises",
],
# ...
)
For Swift, import the module:
import Promises
For Objective-C, import the umbrella header:
#import "path/to/Promises/FBLPromises.h"
Or, the module, if enable_modules = True
:
@import FBLPromises;
In your Package.swift
file, add Promises
dependency to corresponding targets:
let package = Package(
// ...
dependencies: [
.package(url: "https://github.com/google/promises.git", from: "1.2.0"),
],
// ...
)
For Swift, import the module:
import Promises
For Objective-C, import the umbrella header:
#import "FBLPromises.h"
Or, the module, if CLANG_ENABLE_MODULES = YES
:
@import FBLPromises;
To use Promises
for both Swift and Objective-C, add the following to your Podfile
:
pod 'PromisesSwift'
To use Promises
for Objective-C only, add the following to your Podfile
:
pod 'PromisesObjC'
Also, don't forget to add use_frameworks!
to your target. Then, run
pod install
if installing Promises
for the first time or pod update
to
update to the latest version.
For Swift, import the module:
import Promises
For Objective-C, import the module:
@import FBLPromises;
Or, the umbrella header:
#import <FBLPromises/FBLPromises.h>
Add the following to your Cartfile
:
github "google/promises"
Then, run carthage update
and follow the rest of instructions.
Instead of taking a completion block as the last argument:
Swift:
func data(at url: URL, completion: @escaping (Data?, Error?) -> Void)
Objective-C:
- (void)getDataAtURL:(NSURL *)anURL completion:^(NSData *data, NSError *error)completion;
Promises based async routines return a promise object:
Swift:
func data(at url: URL) -> Promise<Data>
Objective-C:
- (FBLPromise<NSData *> *)getDataAtURL:(NSURL *)anURL;
Some legacy code that cannot be modified directly, can be wrapped with
wrap
operator to return a promise.
Also, read more on Objective-C and Swift interoperability specifics.
There are two ways to create a promise depending on whether you need a pending promise that you plan to resolve after some asynchronous work is finished (usual case), or you need an already resolved promise to wrap a value or an error (rare case).
Imagine we have a complex routine which produces a string after lots of computations. It would be nice to run that asynchronously and provide a promise of that string that the clients can observe to get the value or error eventually when completed.
Pass a work block to be called asynchronously in the async
operator and invoke
fulfill()
with a value or reject()
with an error inside that work block when
ready:
Swift:
let promise = Promise<String>(on: .main) { fulfill, reject in
// Called asynchronously on the dispatch queue specified.
if success {
// Resolve with a value.
fulfill("Hello world.")
} else {
// Resolve with an error.
reject(someError)
}
}
Objective-C:
FBLPromise<NSString *> *promise = [FBLPromise onQueue:dispatch_get_main_queue()
async:^(FBLPromiseFulfillBlock fulfill,
FBLPromiseRejectBlock reject) {
// Called asynchronously on the dispatch queue specified.
if (success) {
// Resolve with a value.
fulfill(@"Hello world.");
} else {
// Resolve with an error.
reject(someError);
}
}];
Promises use the main dispatch queue by default, so the above code is actually equivalent to:
Swift:
let promise = Promise<String> { fulfill, reject in
// Called asynchronously on the default queue.
if success {
fulfill("Hello world.")
} else {
reject(someError)
}
}
Objective-C:
FBLPromise<NSString *> *promise = [FBLPromise async:^(FBLPromiseFulfillBlock fulfill,
FBLPromiseRejectBlock reject) {
// Called asynchronously on the default queue.
if (success) {
fulfill(@"Hello world.");
} else {
reject(someError);
}
}];
We can make the above examples even more concise with the do
operator
(which is implemented as a convenience constructor in Swift) if the promise
work block doesn't require async fulfillment:
Swift:
let promise = Promise { () -> String in
// Called asynchronously on the default queue.
guard success else { throw someError }
return "Hello world"
}
Objective-C:
FBLPromise<NSString *> *promise = [FBLPromise do:^id {
// Called asynchronously on the default queue.
return success ? @"Hello world" : someError;
}];
Note: In Swift, the convenience constructor accepting a work block is overloaded
and can return either a value or another promise, which is eventually used to
resolve the newly created promise. In Objective-C, the do
operator return
value is not strongly typed, so you can return a value, another promise or an
error and expect the correct behavior:
Swift:
let promise = Promise { () -> Promise<String> in
// Called asynchronously on the default queue.
guard success else { throw someError }
return someOtherOperation()
}
Objective-C:
FBLPromise<NSString *> *promise = [FBLPromise do:^id {
// Called asynchronously on the default queue.
return success ? [self someOtherOperation] : someError;
}];
And in case you need a pending promise without any async block of work
associated with it, you can use pending()
static func in Swift or
pendingPromise
class method in Objective-C, and resolve the promise manually
later on:
Swift:
let promise = Promise<String>.pending()
// ...
if success {
promise.fulfill("Hello world")
} else {
promise.reject(someError)
}
Objective-C:
FBLPromise<NSString *> *promise = [FBLPromise pendingPromise];
// ...
if (success) {
[promise fulfill:@"Hello world"];
} else {
[promise reject:someError];
}
Beware, though, that creating such an untethered promise may potentially lead to tricky retain cycles.
Sometimes it's convenient to create an already fulfilled or rejected promise. Pass an initial value or error to the promise's constructor for that:
Swift:
func data(at url: URL) -> Promise<Data?> {
if url.absoluteString.isEmpty {
return Promise(nil)
}
return load(url)
}
Objective-C:
- (FBLPromise<NSData *> *)getDataAtURL:(NSURL *)anURL {
if (anURL.absoluteString.length == 0) {
return [FBLPromise resolvedWith:nil];
}
return [self loadURL:anURL];
}
In the examples above a promise resolved with value nil
is returned if the
given URL is empty. Othwerise, we call another routine which returns a promise.
To get notified when a promise is resolved with a value (i.e. is fulfilled) we
need to use the then
operator.
You can fulfill a pending promise in many ways:
- call
fulfill
method on a promise - call
fulfill()
in an async block or return a value (not an error) from the do block - return a value from the
then
block
Or, just create a resolved promise with a non-error value.
The then
operator expects one argument - a block, which has the value that the
promise before it was fulfilled with as an argument, and also expects another
promise, a value, or an error to be returned. The operator itself returns
another promise that will be resolved with the same resolution that the promise
returned from the block. Any value or error returned from the block is
considered a resolved promise initialized with that value or error. For example:
Swift:
let numberPromise = Promise(42)
// Return another promise.
let chainedStringPromise = numberPromise.then { number in
return self.string(from: number)
}
// Return any value.
let chainedStringPromise = numberPromise.then { number in
return String(number)
}
// Throw an error.
let chainedStringPromise = numberPromise.then { number in
throw NSError(domain: "", code: 0, userInfo: nil)
}
// Void return.
let chainedStringPromise = numberPromise.then { number in
print(number)
// Implicit 'return number' here.
}
Note: chainedStringPromise
is an example of a Void
return that is
effectively similar to returning the incoming value, i.e. return number
.
Objective-C:
FBLPromise<NSNumber *> *numberPromise = [FBLPromise resolvedWith:@42];
// Return another promise.
FBLPromise<NSString *> *chainedStringPromise = [numberPromise then:^id(NSNumber *number) {
return [self stringFromNumber:number];
}];
// Return any value.
FBLPromise<NSString *> *chainedStringPromise = [numberPromise then:^id(NSNumber *number) {
return [number stringValue];
}];
// Return an error.
FBLPromise<NSString *> *chainedStringPromise = [numberPromise then:^id(NSNumber *number) {
return [NSError errorWithDomain:@"" code:0 userInfo:nil];
}];
// Fake void return.
FBLPromise<NSString *> *chainedStringPromise = [numberPromise then:^id(NSNumber *number) {
NSLog(@"%@", number);
return nil;
// OR
return number;
}];
Note: Since Objective-C doesn't support method overloading, we cannot provide a
version of the then
operator with void
return type. Thus, if you don't have
a value to return from the then
block, you can always just return nil
or,
even better, the same value as you received. Returning an actual value makes it
easier to chain on this promise in the future.
By default, the then
blocks are dispatched on the
main thread, but they can be easily configured to be dispatched on a custom queue:
Swift:
numberPromise.then(on: backgroundQueue) { number in
return String(number)
}
Objective-C:
[numberPromise onQueue:backgroundQueue then:^id(NSNumber *number) {
return number.stringValue;
}];
But the most important thing, of course, is the ability to chain any number of promises together into a pipeline to simulate synchronous execution:
Swift:
func work1(_ string: String) -> Promise<String> {
return Promise {
return string
}
}
func work2(_ string: String) -> Promise<Int> {
return Promise {
return Int(string) ?? 0
}
}
func work3(_ number: Int) -> Int {
return number * number
}
work1("10").then { string in
return work2(string)
}.then { number in
return work3(number)
}.then { number in
print(number) // 100
}
Since functions in Swift are first-class citizens, we can actually simplify the previous example to:
work1("10").then(work2).then(work3).then { number in
print(number) // 100
}
Objective-C:
- (FBLPromise<NSString *> *)work1:(NSString *)string {
return [FBLPromise do:^id {
return string;
}];
}
- (FBLPromise<NSNumber *> *)work2:(NSString *)string {
return [FBLPromise do:^id {
return @(string.integerValue);
}];
}
- (NSNumber *)work3:(NSNumber *)number {
return @(number.integerValue * number.integerValue);
}
[[[[self work1:@"10"] then:^id(NSString *string) {
return [self work2:string];
}] then:^id(NSNumber *number) {
return [self work3:number];
}] then:^id(NSNumber *number) {
NSLog(@"%@", number); // 100
return number;
}];
To get notified when a promise is resolved with an error (i.e. is rejected), use
the catch
operator.
You can reject a promise in many ways:
- call
reject
method on a promise - call
reject()
in an async block or return an error from the do block - return or throw an error from the
then
block
Or, just create a resolved promise with an error.
catch
operator expects one argument - a block, which has the error that the
promise was rejected with as an argument. The operator itself implicitly returns
another promise, that is rejected with the same error.
Swift:
number(from: "abc").catch { error in
print("Cannot convert string to number: \(error)")
}
Objective-C:
[[self numberFromString:@"abc"] catch:^(NSError *error) {
NSLog(@"Cannot convert string to number: %@", error);
}];
One of the pains of nested completion handlers is the need to branch each time
the previous async task returns an error. With promises, rejection of one
promise is propagated down the pipeline automatically, ignoring any remaining
then
blocks in the pipeline. Instead, the catch
operator can be placed
anywhere in the chain to handle errors. For example, consider the code from
then pipeline:
Swift:
struct CustomError: Error {}
func work1(_ string: String) -> Promise<String> {
return Promise {
return string
}
}
func work2(_ string: String) -> Promise<Int> {
return Promise { () -> Int in
guard let number = Int(string), number > 0 else { throw CustomError() }
return number
}
}
func work3(_ number: Int) -> Int {
return number * number
}
work1("abc").then { string in
return work2(string)
}.then { number in
return work3(number) // Never executed.
}.then { number in
print(number) // Never executed.
}.catch { error in
print("Cannot convert string to number: \(error)")
}
Objective-C:
- (FBLPromise<NSString *> *)work1:(NSString *)string {
return [FBLPromise do:^id {
return string;
}];
}
- (FBLPromise<NSNumber *> *)work2:(NSString *)string {
return [FBLPromise do:^id {
NSInteger number = string.integerValue;
return number > 0 ? @(number) : [NSError errorWithDomain:@"" code:0 userInfo:nil];
}];
}
- (NSNumber *)work3:(NSNumber *)number {
return @(number.integerValue * number.integerValue);
}
[[[[[self work1:@"abc"] then:^id(NSString *string) {
return [self work2:string];
}] then:^id(NSNumber *number) {
return [self work3:number]; // Never executed.
}] then:^id(NSNumber *number) {
NSLog(@"%@", number); // Never executed.
return number;
}] catch:^(NSError *error) {
NSLog(@"Cannot convert string to number: %@", error);
}];
Note: When chaining promises in Objective-C, you can end up having a lot of square brackets. One way to solve that is with dot-syntax.
Having basic operators like async
, do
,
then
and catch
, is normally sufficient to implement most
sequences of async calls with promises. Nevertheless, there're some common
high-level patterns that would also be great to provide out of the box.
all
class method waits for all the provided promises to be fulfilled. Once
they've been fulfilled, the promise returned from all
is fulfilled with an
array of all fulfilled values. If one of the given promises is rejected, then
the returned promise is rejected with the same error.
In Swift, the all
operator also allows passing promises of heterogeneous
types. For this case, the returned promise will be resolved with a tuple
containing the values of the input promises in the same order they were
provided.
Swift:
// Promises of same type:
all(contacts.map { MyClient.getAvatarFor(contact: $0) }).then(updateAvatars)
// Promises of different types:
all(
MyClient.getLocationFor(contact: contact),
MyClient.getAvatarFor(contact: contact)
).then { location, avatar in
self.updateContact(location, avatar)
}
Objective-C:
// Promises of same type:
[[FBLPromise all:[contacts fbl_map:^id(MyContact *contact) {
return [MyClient getAvatarForContact:contact];
}]] then:^id(NSArray<UIImage *> *avatars) {
[self updateAvatars:avatars];
return nil;
}];
// Promises of different types:
[[FBLPromise
all:@[ [MyClient getLocationForContact:contact], [MyClient getAvatarForContact:contact] ]]
then:^id(NSArray *locationAndAvatar) {
[self updateContactLocation:locationAndAvatar.firstObject
andAvatar:locationAndAvatar.lastObject];
return nil;
}];
Note: The Objective-C example above used
-fbl_map
method on NSArray
, which often comes handy, along with other similar
functional operators that
Objective-C lacks.
Note: In Swift, the number of promises of heterogeneous types that all
can handle is 4. If you need more than 4,
you can break the promises down into separate all
and then group them together.
For example:
Swift:
all(all(p1, p2, p3), all(p4, p5, p6)).then { results in
let ((a1, a2, a3), (a4, a5, a6)) = results
print(a1, a2, a3, a4, a5, a6)
}
Also, see how all
helps to avoid nested promises.
always
is handy when we want some piece of code to execute always down the
promises pipeline, regardless of whether or not the previous promise was
fulfilled or rejected.
Swift:
getCurrentUserContactsAvatars().then { avatars in
self.update(avatars)
}.catch { error in
self.showErrorAlert(error)
}.always {
self.label.text = "All done."
}
Objective-C:
[[[[self getCurrentUserContactsAvatars] then:^id(NSArray<UIImage *> *avatars) {
[self updateAvatars:avatars];
return avatars;
}] catch:^(NSError *error) {
[self showErrorAlert:error];
}] always:^{
self.label.text = @"All done.";
}];
any
is similar to all
, but it fulfills even if some of the promises in the
provided array are rejected. If all promises in the input array are rejected,
the returned promise rejects with the same error as the last one that was
rejected.
In Swift the resulting array will contain Maybe
enums that have two cases
.value
and .error
with associated data of either values or errors
corresponding to the resolved promises in same order as they appear in the input
array. In Objective-C the resulting heterogeneous NSArray
will contain values
and errors of resolved input promises as is. In Swift the any
operator also
allows passing promises of heterogeneous types, in which case the resulting
promise will be resolved with a tuple containing the Maybe
enums wrapping
values or errors of the input promises in the same order.
Swift:
// Promises of same type:
any(contacts.map { MyClient.getAvatarFor(contact: $0) }).then { avatarsOrErrors in
self.updateAvatars(avatarsOrErrors.flatMap { $0.value })
}
// Promises of different types:
any(
MyClient.getLocationFor(contact: contact),
MyClient.getAvatarFor(contact: contact)
).then { location, avatar in
if let location = location.value, let avatar = avatar.value {
self.updateContact(location, avatar)
} else { // Optionally handle errors if needed.
if let locationError = location.error {
self.showErrorAlert(locationError)
}
if let avatarError = avatar.error {
self.showErrorAlert(avatarError)
}
}
}
Objective-C:
// Promises of same type:
[[FBLPromise any:[contacts fbl_map:^id(MyContact *contact) {
return [MyClient getAvatarForContact:contact];
}]] then:^id(NSArray *avatarsOrErrors) {
[self updateAvatars:[avatarsOrErrors fbl_filter:^BOOL(id avatar) {
return [avatar isKindOfClass:[UIImage class]];
}]];
return nil;
}];
// Promises of different types:
[[FBLPromise
any:@[ [MyClient getLocationForContact:contact], [MyClient getAvatarForContact:contact] ]]
then:^id(NSArray *locationAndAvatarOrErrors) {
id location = locationAndAvatarOrErrors.firstObject;
id avatar = locationAndAvatarOrErrors.lastObject;
if ([location isKindOfClass:[CLLocation class]] && [avatar isKindOfClass:[UIImage class]]) {
[self updateContactLocation:location andAvatar:avatar];
} else { // Optionally handle errors if needed.
if ([location isKindOfClass:[NSError class]]) {
[self showErrorAlert:location];
}
if ([avatar isKindOfClass:[NSError class]]) {
[self showErrorAlert:avatar];
}
}
return nil;
}];
Note: The Objective-C example above used
-fbl_map
and
-fbl_filter
methods on NSArray
, which often comes handy, along with other similar
functional operators that
Objective-C lacks.
Note: In Swift, the number of promises of heterogeneous types that any
can handle is 3. If you need more than 3,
you can break the promises down into separate any
and then group them together. It is similar to what you can do with all
.
For example:
any( any(p1, p2), any(p3, p4)).then { results in
dump(results) // a tuple containing two MayBe's. each contains their respective MayBe values
}
Using await
you can synchronously wait for a promise to get resolved
on a different thread. That can be useful for situations when you need
to mix several results from multiple async routines differently, i.e.
cannot chain them in a clear pipeline using then
,
all
, etc., or just want to write async code in a sync style.
Swift:
Promise<Int> {
let minusFive = try await(calculator.negate(5))
let twentyFive = try await(calculator.multiply(minusFive, minusFive))
let twenty = try await(calculator.add(twentyFive, minusFive))
let five = try await(calculator.subtract(twentyFive, twenty))
let zero = try await(calculator.add(minusFive, five))
return try await(calculator.multiply(zero, five))
}.then { result in
// ...
}.catch { error in
// ...
}
Objective-C
[[[FBLPromise do:^id {
NSError *error;
NSNumber *minusFive = FBLPromiseAwait([calculator negate:@5], &error);
if (error) return error;
NSNumber *twentyFive = FBLPromiseAwait([calculator multiply:minusFive by:minusFive], &error);
if (error) return error;
NSNumber *twenty = FBLPromiseAwait([calculator add:twentyFive to:minusFive], &error);
if (error) return error;
NSNumber *five = FBLPromiseAwait([calculator subtract:twentyFive from:twenty], &error);
if (error) return error;
NSNumber *zero = FBLPromiseAwait([calculator add:minusFive to:five], &error);
if (error) return error;
NSNumber *result = FBLPromiseAwait([calculator multiply:zero by:five], &error);
if (error) return error;
return result;
}] then:^id(NSNumber *result) {
// ...
}] catch:^(NSError *error) {
// ...
}];
Note: In the above examples it's assumed that all calculator routines are
executed asynchronously on a background thread, because the promise work block
is dispatched on a default queue since no other is
specified, and so any blocking await
would cause a deadlock if it waited for
a promise that was going to be resolved on the default queue as well. Generally,
it's usually safer to use await
from a global concurrent queue only to avoid
any potential deadlocks. Like so:
Promise<Int>(on: .global()) {
try await(object.someAsyncRoutine())
}
Objective-C
[FBLPromise onQueue:dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT)
do:^id {
NSError *error;
id result = FBLPromiseAwait([object someAsyncRoutine], &error);
return error ?: result;
}];
delay
returns a new pending promise that fulfills with the same value as
self
after the given delay, or rejects with the same error immediately. It may
come in handy if you want to add an artificial pause to your promises chain.
race
class method is similar to all
, but the promise that it returns fulfills
or rejects with the same resolution as the first promise that resolves among the
given.
recover
lets us catch
an error and easily recover from it without breaking
the rest of the promise chain.
Swift:
getCurrentUserContactsAvatars().recover { error in
print("Fallback to default avatars due to error: \(error)")
return self.getDefaultsAvatars()
}.then { avatars in
self.update(avatars)
}
Objective-C:
[[[self getCurrentUserContactsAvatars] recover:^id(NSError *error) {
NSLog(@"Fallback to default avatars due to error: %@", error);
return [self getDefaultsAvatars];
}] then:^id(NSArray<UIImage *> *avatars) {
[self updateAvatars:avatars];
return avatars;
}];
reduce
makes it easy to produce a single value from a collection of promises
using a given closure or block. A benefit of using Promise.reduce
over the
Swift library's reduce(_:_:)
,
is that Promise.reduce
resolves the promise with the partial value for you
so you don't have to chain on that promise inside the closure in order to get
its value. Here's a simple example of how to reduce an array of numbers to a
single string:
Swift:
let numbers = [1, 2, 3]
Promise("0").reduce(numbers) { partialString, nextNumber in
Promise(partialString + ", " + String(nextNumber))
}.then { string in
// Final result = 0, 1, 2, 3
print("Final result = \(string)")
}
Objective-C:
NSArray<NSNumber *> *numbers = @[ @1, @2, @3 ];
[[[FBLPromise resolvedWith:@"0"] reduce:numbers
combine:^id(NSString *partialString, NSNumber *nextNumber) {
return [NSString stringWithFormat:@"%@, %@", partialString, nextNumber.stringValue];
}] then:^id(NSString *string) {
// Final result = 0, 1, 2, 3
NSLog(@"Final result = %@", string);
return nil;
}];
retry
provides the flexibility to reattempt a task if the promise associated
with that task is initially rejected. By default, the operator makes a single
retry attempt after the initial rejection with a one second delay before the
reattempt is made. If the default values do not suffice for your use case, the
retry
operator also supports specifying a custom queue to dispatch the work
block to, the max number of retry attempts, a delay time interval, and an optional
predicate for bailing early if the given condition is not met.
Swift:
func fetch(_ url: URL) -> Promise<(Data?, URLResponse?)> {
return wrap { URLSession.shared.dataTask(with: url, completionHandler: $0).resume() }
}
let url = URL(string: "https://myurl.com")!
// Defaults to one retry attempt after a one second delay.
retry { fetch(url) }.then { print($0) }.catch { print($0) }
// Specifies a custom queue, 5 retry attempts, 2 second delay, and a predicate.
let customQueue = DispatchQueue(label: "CustomQueue", qos: .userInitiated)
retry(
on: customQueue
attempts: 5,
delay: 2,
condition: { remainingAttempts, error in
(error as NSError).code == URLError.notConnectedToInternet.rawValue
}
) {
fetch(url)
}.then { values in
// Will enter `then` block if one of the retry attempts succeeds.
print(values)
}.catch { error in
// Will enter `catch` block if all retry attempts have been exhausted or the
// given condition was not met.
print(error)
}
Objective-C:
- (FBLPromise<NSData *, NSURLResponse *> *)fetchWithURL:(NSURL *)url {
return [FBLPromise wrap2ObjectsOrErrorCompletion:^(FBLPromise2ObjectsOrErrorCompletion handler) {
[NSURLSession.sharedSession dataTaskWithURL:url completionHandler:handler];
}];
}
NSURL *url = [NSURL URLWithString:@"https://myurl.com"];
// Defaults to one retry attempt after a one second delay.
[[[FBLPromise retry:^id {
return [self fetchWithURL:url];
}] then:^id(NSArray *values) {
NSLog(@"%@", values);
return nil;
}] catch:^(NSError *error) {
NSLog(@"%@", error);
}];
// Specifies a custom queue, 5 retry attempts, 2 second delay, and a predicate.
dispatch_queue_t customQueue = dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0);
[[[FBLPromise onQueue:customQueue
attempts:5
delay:2.0
condition:^BOOL(NSInteger remainingAttempts, NSError *error) {
return error.code == NSURLErrorNotConnectedToInternet;
}
retry:^id {
return [self fetchWithURL:url];
}] then:^id(NSArray *values) {
// Will enter `then` block if one of the retry attempts succeeds.
NSLog(@"%@", values);
return nil;
}] catch:^(NSError *error) {
// Will enter `catch` block if all retry attempts have been exhausted or the
// given condition was not met.
NSLog(@"%@", error);
}];
timeout
allows us to wait for a promise for a time interval or reject it, if
it doesn't resolve within the given time. A timed out promise rejects with
NSError
in FBLPromiseErrorDomain
domain with code
FBLPromiseErrorCodeTimedOut
.
validate
makes value checks trivial without breaking the promise chain. It
receives a value similar to then
, but returns a boolean indicating if the
value is acceptable. If validate
returns true, the promise is fulfilled with
the value. If it's false, the promise is rejected with NSError
in
FBLPromiseErrorDomain
domain with code FBLPromiseErrorCodeValidationFailure
.
Swift:
getAuthToken().validate { !$0.isEmpty }.then(getData).catch { error in
print("Failed to get auth token: \(error))
}
Objective-C:
[[[[self getAuthToken] validate:^BOOL(NSString *authToken) {
return authToken.length > 0;
}] then:^id(NSString *authToken) {
return [self getDataWithToken:authToken];
}] catch:^(NSError *error) {
NSLog(@"Failed to get auth token: %@", error);
}];
wrap
class method provides a convenient way to convert other methods that
use common callback patterns (like ^(id, NSError *)
, etc.) into promises.
Swift:
func newAsyncMethodReturningAPromise() -> Promise<Data> {
return wrap { handler in
MyClient.wrappedAsyncMethodWithTypical(completion: handler)
}
}
Objective-C:
- (FBLPromise<NSData*> *)newAsyncMethodReturningAPromise {
return [FBLPromise wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion handler) {
[MyClient wrappedAsyncMethodWithTypicalCompletion:handler];
}];
}
Promises use GCD
internally and make all APIs provide a way to specify which dispatch queue each
block of work should be dispatched on. Main queue is the default, if one isn't
specified. Setting the default dispatch queue to any other than the main is
normally needed when the main one is busy serving some custom event run loop,
but not the standard for Apple platforms CFRunLoop
. That situation is pretty
common for different server-side frameworks, that similarly to AppKit/UIKit,
also implement the inversion of control
design principle and park the main thread in a custom run loop. To specify the
default dispatch queue, add the following line to the beginning of your program:
Swift:
DispatchQueue.promises = .global()
Objective-C:
FBLPromise.defaultDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
You rarely need to care about retain cycles with Promises. When chaining fulfillment or rejection observers, or any other convenience extensions, the returned promise has a strong reference to the chained work block. But once the promise gets resolved it removes any references to observer blocks that were chained on it and schedules them on GCD. Thus, GCD is the one which owns all blocks and everything captured in them until those blocks are eventually executed.
Nevertheless, beware that you can create a retain cycle if you use a promise object inside a block chained on it. That's possible if you've stored the promise in a local var or ivar, especially if you've created a pending promise without any work block associated with it. That cycle won't be broken until the promise gets resolved. Consider the following example:
Swift:
class MyClass {
var promise: Promise<Int>?
func doSomething() -> Promise<String> {
if promise == nil {
promise = Promise<Int>.pending()
}
return promise?.then(doSomethingElse)
}
func doSomethingElse(number: Int) -> String {
return String(number)
}
}
Objective-C:
@implementation MyClass {
FBLPromise<NSNumber *> *_promise;
}
- (FBLPromise<NSString *> *)doSomething {
if (_promise == nil) {
_promise = [FBLPromise pendingPromise];
}
return [_promise then:^id(id number) {
return [self doSomethingElse:number];
}];
}
- (NSString *)doSomethingElse:(NSNumber *)number {
return number.stringValue;
}
@end
self
owns the promise
, and promise
in turn captures self
in then
block
until it gets eventually resolved, if ever. So we get a retain cycle. We could
resolve it with a weak reference, of course, since we're aware of code specifics
in MyClass
. But that situation can become even more subtle:
Swift:
myClass.doSomething().then { string in
myClass.doSomeOtherThing()
}
Objective-C:
[[myClass doSomething] then:^id(NSString *string) {
[myClass doSomeOtherThing];
}];
Here we get a promise from one of MyClass
methods and use it to chain an
observer block which in turn captures that MyClass
instance inside. Therefore,
myClass
owns the promise, which owns the block, which captures myClass
to
invoke some other method. The tricky part is that the code which uses MyClass
may never know it has a strong reference to the promise returned from
doSomething
method and, moreover, there's no code that's dedicated to resolve
that promise soon, because we've used pending
constructor rather
than async
or do
.
There's probably no silver bullet to avoid retain cycles like those and each
case should be considered individually. Just try to avoid pending
promises where possible and always resolve your promises as soon as possible, so
that the ownership of your observer blocks and everything they may have captured
can be handled by GCD.
Unit tests are typically run in a single thread of execution. So waiting for a
bunch of async tasks to finish in a test can be tricky. To facilitate that, all
promises are dispatched in a common dispatch group (DispatchGroup.promises
in
Swift or FBLPromise.dispatchGroup
in Objective-C) that you can wait on with a
helper function (waitForPromises()
in Swift or
FBLWaitForPromisesWithTimeout()
in Objective-C):
Swift:
@testable import Promises
// ...
func testExample() {
// Arrange & Act.
let promise = Promise<Int> { 42 }
// Assert.
XCTAssert(waitForPromises(timeout: 1))
XCTAssertEqual(promise.value, 42)
XCTAssertNil(promise.error)
}
// ...
Objective-C:
#import "path/to/Promises/FBLPromise+Testing.h"
// ...
- (void)testExample {
// Arrange & Act.
FBLPromise<NSNumber *> *promise = [FBLPromise do:^id {
return @42;
}];
// Assert.
XCTAssert(FBLWaitForPromisesWithTimeout(1));
XCTAssertEqualObjects(promise.value, @42);
XCTAssertNil(promise.error);
}
// ...
Those functions take a timeout arg and return true if all promise blocks have completed before the timeout; otherwise, they return false.
To run the test suite for the Promises framework itself it's recommended to use
the Xcode project: select FBLPromises
or Promises
target and hit ⌘+U.
To test with Bazel, cd
to the project directory and run:
bazel test Tests
A bit limited set of tests can also be run with Swift Package Manager:
swift test
The class Promise
in Swift is essentially a wrapper over the Objective-C
FBLPromise
class. Thus, as an addition to standard ways of creating
promises, in Swift you can pass FBLPromise
object into
Promise
constructor and also access an underlying FBLPromise
instance with
asObjCPromise()
method:
@interface ObjCTest : NSObject
- (FBLPromise<NSString *> *)getString;
- (FBLPromise<NSNumber *> *)getNumber:(NSString *)string;
- (void)asyncWith:(NSString *)string and:(NSInteger)integer completion:(void(^)())handler;
- (void)needsAPromise:(FBLPromise<NSString *> *)promise;
@end
Here's how we could use ObjCTest
in Swift:
let objc = ObjCTest()
Promise<String>(objc.getString()).then { string in
return Promise<Int>(objc.getNumber(string))
}.then { number in
print(number)
}
wrap { handler in
objc.async(with: "hello", and: 42, completion: handler)
}.then { _ in
print("Success.")
}.catch { error in
print(error)
}
let stringPromise = Promise<String> {
return "Hello world!"
}
objc.needsAPromise(stringPromise.asObjCPromise())
@objc(providesAPromiseFromNumber:)
func providesAPromise(from number: Int) -> Promise<String>.ObjCPromise<NSString> {
return Promise<String> {
"The number is \(number)"
}.asObjCPromise()
}
objc.needsAPromise(providesAPromise(42))
When chaining promises in Objective-C, you can end up having a lot of square brackets and other formatting concerns. To help with that, we used a trick which lets us pass args into a method via its return value of block type. For example, the code in catch pipeline written using the dot-syntax would look like:
[self work1:@"abc"]
.then(^id(NSString *string) {
return [self work2:string];
})
.then(^id(NSNumber *number) {
return [self work3:number];
})
.then(^id(NSNumber *number) {
NSLog(@"%@", number);
return nil;
})
.catch(^(NSError *error) {
NSLog(@"Cannot convert string to number: %@", error);
});
All Objective-C Promises APIs provide convenience methods for using dot-syntax.
Promises are very simple once you grok some basics, but there are a few gotchas to avoid.
You have code like:
Swift:
func asyncCall() -> Promise<Data> {
let promise = doSomethingAsync()
promise.then(processData)
return promise
}
Objective-C:
- (FBLPromise<NSData *> *)asyncCall {
FBLPromise<NSData *> *promise = [self doSomethingAsync];
[promise then:^id(NSData *result) {
return [self processData:result];
}];
return promise;
}
The problem here is that if the promise returned from processData
method is
rejected, there's no way to catch
it. Promises are meant to be chained. To
fix, always return the result of the final then
:
Swift:
func asyncCall() -> Promise<Data> {
let promise = doSomethingAsync()
return promise.then(processData)
}
Objective-C:
- (FBLPromise<NSData *> *)asyncCall {
FBLPromise<NSData *> *promise = [self doSomethingAsync];
return [promise then:^id(NSData *result) {
return [self processData:result];
}];
}
Avoid nesting promises, as this is the issue that promises are designed to solve:
Swift:
loadSomething().then { something in
self.loadAnother().then { another in
self.doSomething(with: something, and: another)
}
}
Objective-C:
[[self loadSomething] then:^id(NSData *something) {
return [[self loadAnother] then:^id(NSData *another) {
return [self doSomethingWith:something andAnother:another];
}];
}];
The reason why this issue even appeared is because we need to do something with
the results of both promises, so we can’t chain them, since the then
is only
passed the result of the previous return. Thankfully, there's all
operator:
Swift:
all([loadSomething(), loadAnother()]).then { result in
self.doSomething(with: result.first, and: result.last)
}
Objective-C:
[[FBLPromise all:@[ [self loadSomething], [self loadAnother] ]] then:^id(NSArray<NSData *> *result) {
return [self doSomethingWith:result.firstObject andAnother:result.lastObject];
}];
And if you don't like all
(an array of heterogeneous values doesn't always
read well), just move the nested part into a separate method:
Swift:
loadSomething().then { something in
self.loadAnother(with: something)
}
func loadAnother(with something: Data) -> Promise<MyResult> {
loadAnother().then { another in
self.doSomething(with: something, and: another)
}
}
Objective-C:
[[self loadSomething] then:^id(NSData *something) {
return [self loadAnotherWithSomething:something];
}];
- (FBLPromise<MyResult *> *)loadAnotherWithSomething:(NSData *)something {
return [[self loadAnother] then:^id(NSData *another) {
return [self doSomethingWith:something andAnother:another];
}];
}