View on GitHub

Promisekit

Promises for iOS and OS X

Download this project as a .zip file Download this project as a tar.gz file

Promises Cookbook

Recipe 0: Understand return values

The key to understanding promises is to understand how return values move through the promise resolution process. Each of the recipes, below, illustrates a different aspect of this.

A promise is essentially a function pointer. You can pass the function pointer around, but you don't get the value out until you call it. The way to call a promise is to then it. However, because the function is asynchronous, it can't pass its return value to the left. Instead, it passes it into its then block.

Recipe 1: Get the value from a promise

// Method signature

// Promise resolves to User instance
- (PMKPromise *)loginToRemoteServer:(NSString *)userId; 
// Getting the value out.
[self loginToRemoteServer:userId].then(^(User *user) {
    self.user = user;
});

Notes

Recipe 2: Handle errors

Every promise actually has the potential to return two different types: its "promised" type and NSError. If the promise fails, the then block is skipped.

[self loginToRemoteServer:userId].then(^(User *user) {
    self.user = user;
}).catch(^(NSError *error){
    // Something went wrong; deal with it here
    ...
    return nil;
});

Notes

Recipe 3: Create a sequence of promises

A single promise is basically a fancy callback with a catch block. The power of promises comes from the fact that they are chainable (aka composable).

First, look at the unchained (ie., nested) version. It is very similar to nested callbacks and just as with callbacks, if we added error handling to each promise, it would start to get messy.

// Method signatures

// Promise resolves to User instance
- (PMKPromise *)loginToRemoteServer:(NSString *)userId; 

// Promise resolves to NSDictionary of user data
- (PMKPromise *)retrieveUserData:(User *)user; 

// Promise resolves to UIImage
- (PMKPromise *)retrieveProfileImage:(NSURL *)imageURL; 
[self loginToRemoteServer:userId].then(^(User *user) {
    self.user = user;
    [self retrieveUserData:user].then(^(NSDictionary *userData){
         self.user.data = userData;
         [self retrieveProfileImage:userData.profileImageURL].then(^(UIIMage *userImage){
               self.user.profileImage = userImage;
         });
    });
})

Here is the chained version. Note the single catch block that simplifies error handling.

[self loginToRemoteServer:userId].then(^(User *user) {
    self.user = user;
    return [self retrieveUserData:user];
}).then(^(NSDictionary *userData){
    self.user.data = userData;
    return [self retrieveProfileImage:userData.profileImageURL];
}).then(^(UIImage *userImage){
    self.user.profileImage = userImage;
}).catch(^(NSError *){
    // Catch any errors here
    ...
});

Notes: Rules for chained return values

Notice that in the chained version, each of the then blocks has a return value. Here's how it works:

And here's the most important part:

Recipe 4: Branching

myURLPromise.then(^id(NSURL *url){ 
     if (url) {
         return url; // Got my url, I'm done, return the value
     }
     else {
         return myOtherURLPromise; // Not done, do some more work
     }
}).then(^(NSURL *url){
    // Do something with URL ...
});

Notes

Until now, the method signatures of our then blocks have all omitted their return types. These return types are being filled in via introspection. But whenever a block returns more than one type (as may happen when branching), the return type must be explicitly typed to id.

In this example, the first return value is NSURL * but the second is PMKPromise *. The compiler won't accept that unless we specify the return type of the block as id.

Recipe 5: Looping

You want to get the results from a set of promises whose number is not known in advance. Use the when class method to handle multiple promises at the same time.

// Promise resolves to array of UIImages
- (PMKPromise *)getImagesForURLs:(NSArray *)urls
{
    NSMutableArray *promises = [NSMutableArray array];
    for (NSURL *url in urls) {
        // getImageForURL returns promise that resolves to UIImage
        [promises addObject:[self getImageForURL:url]]; 
    }
    return [PMKPromise when:promises].then(^(NSArray *results){ 
            NSMutableArray *images = [NSMutableArray array];
            for (UIImage *image in results) {
                [images addObject:image];
            }
            return images;
    });
}

Recipe 6: Pass primitives to then block.

You can return primitives from a then block, but the arguments passed into the next block are always boxed (ie., wrapped as objects).

myPromise.then(^{
        ...
        return 3;
}).then(^(NSNumber *result){
    NSInteger intResult = [result integerValue];
    ...
});

Recipe 7: Wrap an asynchronous method inside a promise

Any method that takes a callback (block) can be turned into a promise by wrapping it. Here, we're wrapping Firebase's oauth method.

// Promise resolves to FAuthData.
- (PMKPromise *)firebaseAuthWithOAuthProvider:provider accessToken(NSString *)token
{
    PMKPromise *promise = [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
        [firebase authWithOAuthProvider:provider token:token withCompletionBlock:^(NSError *error, FAuthData *authData) {
            if (error) {
                reject(error);                            
            }
            else {
                fulfill(authData);
            }
        }];
    }];
    return promise;
}

Recipe 8: Use promises as building blocks

// Promise returns User instance
- (PMKPromise *)setupUserWithId:(NSString *)userId
{
    PMKPromise *promise;
    if (self.user) {
        promise = [PMKPromise promiseWithValue:self.user]; // Resolves immediately
    }
    else {
        promise = [self loginToRemoteServer:userId];
    }
    return promise.then(^(User *user) {
        self.user = user;
        return [self retrieveUserData:user];
    }).then(^(NSDictionary *userData){
        self.user.data = userData;
        return [self retrieveProfileImage:userData.profileImageURL];
    }).then(^(UIImage *userImage){
        self.user.profileImage = userImage;
        return self.user; // Return set up User as promise fulfillment
    }).catch(^(NSError *){
         // Log error but re-throw so caller can handle it
         // If we returned nil here, error would be considered handled
         NSLog(@"Error in setupUserWithId: %@", error);
         return error;
    });
}

... elsewhere ...

[self setupUserWithId:userId].then(^(User *user){
    // User is set up; do something with it
}).catch(^(NSError *error){
    // Let app user know that there's a problem
});