Skip to content

AlexSeverinov/PromiseKit

 
 

Repository files navigation

Modern development is highly asynchronous: isn’t it about time iOS developers had tools that made programming asynchronously powerful, easy and delightful?

PromiseKit is not just a Promises implementation, it is also a collection of helper functions that make the typical asynchronous patterns we use in iOS development delightful too.

PromiseKit is also designed to be integrated into other CocoaPods. If your library has asynchronous operations and you like PromiseKit, then add an opt-in subspec that provides Promises for your users. Documentation to help you integrate PromiseKit into your own pods is provided later in this README.

#Using PromiseKit

In your Podfile:

pod 'PromiseKit'

PromiseKit is modulized; if you don’t want any of our category additions:

pod 'PromiseKit/base'

Or if you only want some of our categories:

pod 'PromiseKit/Foundation'
pod 'PromiseKit/UIKit'
pod 'PromiseKit/CoreLocation'

#What’s This All About?

Synchronous code is clean code. For example, here's the synchronous code to show a gravatar image:

NSString *md5 = md5(email);
NSString *url = [@"http://gravatar.com/avatar/" stringByAppendingString:md5];
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:url]];
self.imageView.image = [UIImage imageWithData:data];

Clean but blocking: the UI lags: the user rates you one star.

The asynchronous analog suffers from rightward-drift:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSString *md5 = md5(email);
    NSString *url = [@"http://gravatar.com/avatar/" stringByAppendingString:md5];
    NSURLRequest *rq = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
    [NSURLConnection sendAsynchronousRequest:rq queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        UIImage *gravatarImage = [UIImage imageWithData:data];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = gravatarImage;
        });
    }];
});

The code that does the actual work is now buried inside asynchronicity boilerplate. It is harder to read. The code is less clean.

Promises are chainable, standardized representations of asynchronous tasks. The equivalent code with PromiseKit looks like:

#import "PromiseKit.h"

dispatch_promise(^{
    return md5(email);
}).then(^(NSString *md5){
    return [NSURLConnection GET:@"http://gravatar.com/avatar/%@", md5];
}).then(^(UIImage *gravatarImage){
    self.imageView.image = gravatarImage;
});

Code with promises is about as close as we can get to the minimal cleanliness of synchronous code. PromiseKit also provides Promise solutions for iOS components that otherwise have no synchronous analog, like CLLocationManager and UIAlertView which usually require a delegation pattern.

The above code dispatches a promise to a background queue (where it computes the md5), the md5 is then input to the next Promise which returns a new Promise that downloads the gravatar. If you return a Promise from a then block the next Promise (ie. the Promise returned by the then) waits (asynchronously) for that Promise to fulfill before it executes its then blocks. PromiseKit’s NSURLConnection category methods automatically decode images in a background thread before passing them to the next Promise.

#Error Handling

Synchronous code has simple, clean error handling:

extern id download(id url);

@try {
    id json1 = download(@"http://api.service.com/user/me");
    id uname = [json1 valueForKeyPath:@"user.name"];
    id json2 = download([NSString stringWithFormat:@"http://api.service.com/followers/%@", uname]);
    self.userLabel.text = @(json2[@"count"]).description;
} @catch (NSError *error) {
    //
}

id download(id url) {
    id url = [NSURL URLWithString:@"http://api.service.com/user/me"]
    id data = [NSData dataWithContentsOfURL:self.url];
    id json = [NSJSONSerialization JSONObjectWithData:data error:&error];
    if (error) @throw error;
}

Error handling with asynchronous code is notoriously tricky:

void (^errorHandler)(NSError *) = ^(NSError *error){
    //
};

id url = @"http://api.service.com/user/me";
id rq = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
[NSURLConnection sendAsynchronousRequest:rq queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
    if (connectionError) {
        errorHandler(connectionError);
    } else {
        dispatch_async(bgq, ^{
            id jsonError = nil;
            id json = [NSJSONSerialization JSONObjectWithData:data error:&jsonError]
            dispatch_async(mainq, ^{
                if (jsonError) {
                    errorHandler(jsonError);
                } else {
                    id uname = [json valueForKeyPath:@"user.name"];
                    id url = [NSString stringWithFormat:@"http://api.service.com/followers/%@", uname];
                    id rq = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
                    [NSURLConnection sendAsynchronousRequest:rq queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
                        if (connectionError) {
                            errorHandler(connectionError);
                        } else {
                            dispatch_async(bgq, ^{
                                id jsonError = nil;
                                id json = [NSJSONSerialization JSONObjectWithData:data error:&jsonError]
                                dispatch_async(mainq, ^{
                                    if (jsonError) {
                                        errorHandler(jsonError);
                                    } else {
                                        self.userLabel.text = @(json2[@"count"]).description;
                                    }
                                });
                            });
                        }
                    }
                }
            });
        });
    }
}];

Wow! Such rightward-drift. To be fair the above could be simplified, but without creating your own NSOperationQueue and without using early-return statements and without DRYing out something as common as deserialzing some downloaded JSON, this is what you get. In fact standard asynchronicity handling in iOS practically encourages you to deserialize the JSON on the main thread—simply to avoid rightward-drift.

##Promises Have Elegant Error Handling

#import "PromiseKit.h"

[NSURLConnection GET:@"http://api.service.com/user/me"].then(^(id json){
    id name = [json valueForKeyPath:@"user.name"];
    return [NSURLConnection GET:@"http://api.service.com/followers/%@", name];
}).then(^(id json){
    self.userLabel.text = @(json[@"count"]).description;
}).catch(^(NSError *error){
    //
});

Raised exceptions or NSError objects returned from handlers bubble up to the first catch handler in the chain.

PromiseKit’s NSURLConnection additions correctly propogate errors for you (as well as decoding the JSON automatically in a background thread based on the mime-type the server returns).

#Say Goodbye to Asynchronous State Machines

Promises represent the future value of a task. You can add more than one then handler to a promise. Even after the promise has been fulfilled. If the promise already has a value, the then handler is called immediately:

@implementation MyViewController {
    Promise *gravatar;
}

- (void)viewDidLoad {
    gravatar = dispatch_promise(^{
        return md5(email);
    }).then(^(NSString *md5){
        return [NSURLConnection GET:@"http://gravatar.com/avatar/%@", md5];
    });

    gravatar.then(^(UIImage *image){
        self.imageView.image = image;
    });
}

- (void)someTimeLater {
    gravatar.then(^(UIImage *image){
        // likely called immediately, but maybe not. We don’t have to worry!
        self.otherImageView.image = image;
    });
}

@end

A key understanding is that Promises can only exist in two states, pending or fulfilled. The fulfilled state is either a value or an NSError object. A Promise can move from pending to fulfilled exactly once.

#Waiting on Multiple Asynchronous Operations

One powerful reason to use asynchronous variants is so we can do two or more asynchronous operations simultaneously. However writing code that acts when the simultaneous operations have all completed is hard. Not so with PromiseKit:

id grabcat = [NSURLConnection GET:@"http://placekitten.org/%d/%d", w, h];
id locater = [CLLocationManager promise];

[Promise when:@[grabcat, locater]].then(^(NSArray *results){
    // results[0] is the `UIImage *` from grabcat
    // results[1] is the `CLLocation *` from locater
}).catch(^(NSError *error){
    // with `when`, if any of the Promises fail, the `catch` handler is executed
    NSArray *suberrors = error.userInfo[PMKThrown];

    // `suberrors` may not just be `NSError` objects, any promises that succeeded
    // have their success values passed to this handler also. Thus you could
    // return a value from this `catch` and have the Promise chain continue, if
    // you don't care about certain errors or can recover.
});

#Forgiving Syntax

In case you didn't notice, the block you pass to then or catch can have return type of Promise, or any object, or nothing. And it can have a parameter of id, or a specific class type, or nothing.

So, these are all valid:

myPromise.then(^{
    //
});

myPromise.then(^(id obj){
    //
});

myPromise.then(^(id obj){
    return @1;
});

myPromise.then(^{
    return @2;
});

Clang is smart so you don’t (usually) have to specify a return type for your block.

This is not usual to Objective-C or blocks. Usually everything is very explicit. We are using introspection to determine what arguments and return types you are working with. Thus, programming with PromiseKit has similarities to programming with more modern languages like Ruby or Javascript.

#The Niceties

PromiseKit aims to provide a category analog for all one-time asynchronous features in the iOS SDK (eg. not for UIButton actions, Promises fulfill once so some parts of the SDK don’t make sense as Promises).

An additional important consideration is that we only trigger the catch handler for errors. Thus UIAlertView does not trigger the catch handler for cancel button pushes. Initially we had it that way, and it led to error handling code that was messy and unreliable. The error path is only for errors.

##NSURLConnection+PromiseKit

#import "PromiseKit+Foundation.h"

[NSURLConnection GET:@"http://promisekit.org"].then(^(NSData *data){
    
}).catch(^(NSError *error){
    NSHTTPURLResponse *rsp = error.userInfo[PMKURLErrorFailingURLResponse];
    int HTTPStatusCode = rsp.statusCode;
});

And a convenience string format variant:

[NSURLConnection GET:@"http://example.com/%@", folder].then(^{
    //
});

And a variant that constructs a correctly URL encoded query string from a dictionary:

[NSURLConnection GET:@"http://example.com" query:@{@"foo": @"bar"}].then(^{
    //
});

And a POST variant:

[NSURLConnection POST:@"http://example.com" formURLEncodedParameters:@{@"key": @"value"}].then(^{
    //
});

PromiseKit reads the response headers and tries to be helpful:

[NSURLConnection GET:@"http://example.com/some.json"].then(^(NSDictionary *json){
    assert([json isKindOfClass:[NSDictionary class]]);
});

[NSURLConnection GET:@"http://placekitten.org/100/100"].then(^(UIImage *image){
    assert([image isKindOfClass:[UIImage class]]);
});

Otherwise we return the raw NSData.

And of course a variant that just takes an NSURLRequest *:

NSMutableURLRequest *rq = [NSMutableURLRequest requestWithURL:url];
[rq addValue:@"PromiseKit" forHTTPHeader:@"User-Agent"]; 
[NSURLConnetion promise:rq].then(^(NSData *data){
    //
})

##NSURLCache+PromiseKit

Sometimes you just want to query the NSURLCache because doing an NSURLConnection will take too long and just return the same data anyway. We perform the same header analysis as the NSURLConnection categories, so eg. you will get back a UIImage * or whatever. If there is nothing in the cache, then you get back nil.

#import "PromiseKit+Foundation.h"

[[NSURLCache sharedURLCache] promisedResponseForRequest:rq].then(^(id o){
    return o ?: [NSURLConnection GET:rq];
});

##CLLocationManager+PromiseKit

A promise for a one time update of the user’s location:

#import "PromiseKit+CoreLocation.h"

[CLLocationManager promise].then(^(CLLocation *currentUserLocation){
});

##UIAlertView+PromiseKit

A promise for showing a UIAlertView:

#import "PromiseKit+UIKit.h"

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"You Didn’t Save!"
                      message:@"You will lose changes."
                     delegate:nil
            cancelButtonTitle:@"Cancel"
            otherButtonTitles:@"Lose Changes", @"Panic", nil];

alert.promise.then(^(NSNumber *dismissedIndex){
    //
});

##UIActionSheet+PromiseKit

Same pattern as for UIAlertView.

##UIViewController+PromiseKit

We provide a pattern for modally presenting ViewControllers and getting back a result:

#import "PromiseKit+UIKit.h"

@implementation MyRootViewController

- (void)foo {
    UIViewController *vc = [MyDetailViewController new];
    [self promiseViewController:vc animated:YES completion:nil].then(^(id result){
        // the result from below in `someTimeLater`
        // PromiseKit dismisses the MyDetailViewController instance when the
        // Deferred is resolved
    });
}

@end

@implementation MyDetailViewController
@property Deferred *deferred;

- (void)viewWillDefer:(Deferred *)deferMe {
    // Deferred is documented below this section
    _deferred = deferMe;
}

- (void)someTimeLater {
    [_deferred resolve:someResult];
}

@end

As a bonus we handle some of the tedious special ViewController types for you so you don't have to delegate. Currently just MFMailComposeViewController. So you can then off of it without having to write any delegate code:

id mailer = [MFMailComposerViewController new];
[self promiseViewController:mailer animated:YES completion:nil].then(^(NSNumber  *num){
    // num is the result passed from the MFMailComposeViewControllerDelegate
}).catch(^{
    // the error from the delegate if that happened
})

Check out Promise.h for more documentation.

#Deferred

If you want to write your own methods that return Promises then often you will need a Deferred object. Promises are deliberately opaque: you can't directly modify them, only their parent promise can.

A Deferred has a promise, and using a Deferred you can set that Promise's value, the Deferred then recursively calls any sub-promises. For example:

- (Promise *)tenThousandRandomNumbers {
    Deferred *d = [Deferred new];

    dispatch_async(q, ^{
        NSMutableArray *numbers = [NSMutableArray new];
        for (int x = 0; x < 10000; x++)
            [numbers addObject:@(arc4random())];
        dispatch_async(dispatch_get_main_queue(), ^{
            if (logic) {
                [d resolve:numbers];
            } else {
                [d reject:[NSError errorWith…]];
            }
        });
    });

    return d.promise;
}

- (void)viewDidLoad {
    [self tenThousandRandomNumbers].then(^(NSMutableArray *numbers){
        //
    });
}

Although for the common case of an operation that runs in the background we offer the convenience function dispatch_promise, which is like dispatch_async, but returns a Promise (which continues on the main queue). So the above would be:

- (Promise *)tenThousandRandomNumbers {
    return dispatch_promise(^{
        NSMutableArray *numbers = [NSMutableArray new];
        for (int x = 0; x < 10000; x++)
            [numbers addObject:@(arc4random())];
        return numbers;
    });
}

dispatch_promise runs on DISPATCH_QUEUE_PRIORITY_DEFAULT. If you need another queue we also provide: dispatch_promise_on.

#The Fine Print

The fine print of PromiseKit is mostly exactly what you would expect, so don’t confuse yourself: only come back here when you find yourself curious about more advanced techniques.

  • Returning a Promise as the value of a then (or catch) handler will cause any subsequent handlers to wait for that Promise to fulfill.
  • Returning an instance of NSError or throwing an exception within a then block will cause PromiseKit to bubble that object up to the nearest catch handler.
  • catch handlers always are passed an NSError object.
  • Returning something other than an NSError from a catch handler causes PromiseKit to consider the error resolved, and execution will continue at the next then handler using the object you returned as the input.
  • Not returning from a catch handler (or returning nil) causes PromiseKit to consider the Promise complete. No further bubbling occurs.
  • Nothing happens if you add a then to a failed Promise
  • Adding a catch handler to a failed Promise will execute that fail handler: this is converse to adding the same to a pending Promise.

#Adding Promises to Third Party Libraries

It would be great if every library with asynchronous functionality would offer opt-in Promise * variants for the asynchronous mechanisms.

Should you want to add PromiseKit integration to your library, the general premise is to add an opt-in subspec to your podspec that provides methods that return Promises. For example if we imagine a library that overlays a kitten on an image:

@interface ABCKitten
- (instancetype)initWithImage:(UIImage *)image;
- (void)overlayKittenWithCompletionBlock:(void)(^)(UIImage *, NSError *))completionBlock;
@end

Opt-in PromiseKit support would include a new file ABCKitten+PromiseKit.h:

#import <PromiseKit/Promise.h>
#import "ABCKitten.h"


@interface ABCKitten (PromiseKit)

/**
 * Returns a Promise that overlays a kitten image.
 * @return A Promise that will then a `UIImage *` object.
 */
- (Promise *)overlayKitten;

@end

It's crucially important to document your Promise methods properly, because the result of a Promise can be any object type and your users need to be able to easily look up the types by ⌥ clicking the method.

Consumers of your library would then include in their Podfile:

pod 'ABCKitten/PromiseKit'

This is the “opt-in” step.

Finally you need to modify your podspec. If it was something like this:

Pod::Spec.new do |s|
  s.name         = "ABCKitten"
  s.version      = "1.1"
  s.source_files = 'ABCKitten.{m,h}'
end

Then you would need to convert it to the following:

Pod::Spec.new do |s|
  s.name         = "ABCKitten"
  s.version      = "1.1"

  s.default_subspec = 'base'  # ensures that we are opt-in

  s.subspec 'base' do |ss|
    ss.source_files = 'ABCKitten.{m,h}'
  end

  s.subspec 'PromiseKit' do |ss|
    ss.dependency 'PromiseKit/base', 'ABCKitten/base'
    ss.source_files = 'ABCKitten+PromiseKit.{m,h}'
  end
end

As a further example, the actual implementation of - (Promise *)overlayKitten would likely be as simple as this:

- (Promise *)overlayKitten {
    Deferred *deferred = [Deferred new];
    [self overlayKittenWithCompletionBlock:^(UIImage *img, NSError *err){
        if (err)
            [deferred reject:err];
        else
            [deferred resolve:img];
    }];
    return deferred.promise;
}

#Adding PromiseKit to Someone Else’s Pod

Firstly you should try submitting the above to the project itself. If they won’t add it then you'll need to make your own pod. Use the naming scheme: ABCKitten+PromiseKit.

#Why PromiseKit?

There are other Promise implementations for iOS, but in this author’s opinion, none of them are as pleasant to use as PromiseKit.

  • Bolts was the inspiration for PromiseKit. I thought that—finally—someone had written a decent Promises implementation for iOS. The lack of dedicated catch handler, the (objectively) ugly syntax and the overly complex design was a disappointment. To be fair Bolts is not a Promise implementation, it’s… something else. You may like it, and certainly it is backed by big names™. Fundamentally, Promise-type implementations are not hard to write, so you really are making a decision based on how flexible the API is while simulatenously producing readable, clean code. I have worked hard to make PromiseKit the best choice.
  • RXPromise is an excellent Promise implementation that is mostly let down by syntax choices. By default thens are executed in background threads, which usually is inconvenient. then always return id and always take id, which makes code less elegant. There is no explicit catch, instead then always takes two blocks, the second being the error handler, which is ugly. The interface for Promise allows any caller to resolve it breaking encapsulation. Otherwise an excellent implementation.
  • Many others

PromiseKit is well tested, and inside apps on the store. It also is fully documented, even within Xcode (⌥ click any method).

#Caveats

  • We are version 0.9 and thus reserve the right to remove/change API before 1.0. Probably we won’t; we’re just being prudent by stating this advisory.
  • PromiseKit is not thread-safe. This is not intentional, we will fix that. However, in practice the only way to compromise PromiseKit is to keep a pointer to an unresolved Promise and use that from multiple threads. You can execute thens in many different contexts and the underlying immutability of Promises means PromiseKit is inherently thread-safe.

About

A delightful Promises implementation for iOS

Resources

License

Stars

Watchers

Forks

Packages

No packages published