Skip to content

Latest commit

 

History

History
279 lines (222 loc) · 10.6 KB

README.md

File metadata and controls

279 lines (222 loc) · 10.6 KB

OMPromises Supported Platforms Version Build Status

A tested and fully documented promises library inspired by Promises/A with certain additions and changes to better fit the common Objective-C patterns.

If you are completely unfamiliar with promises, I recommend you to read some articles and tutorials like one of 1, 2, 3 or 4. Some method names might differ but the idea is mostly the same.

Features

The main features of OMPromises are listed below.

  • Fully tested and documented
  • Clean interface and separation of concerns
  • Support for chaining and callbacks, using then:, rescue:, fulfilled:, failed: and progressed:, similar to most other libraries
  • Chaining blocks are protected against exceptions
  • Support for progress notifications, unlike other libraries
  • Optional support for cancellation
  • Queue based block execution if needed
  • Various combinators

Installation

The recommended approach for installing OMPromises is to use CocoaPods package manager.

pod 'OMPromises', '~> 0.8.1'

Documentation

All public classes, methods, properties and types are documented, thus each copy of OMPromises comes with a full documentation found in the corresponding header file.

An online and much more readable version, rendered using appledoc, can be found here.

Overview

Creation - Making promises

Promises are represented by objects of type OMPromise. Creating already fulfilled, failed or delayed promises that might get fulfilled or fail is as simple as using one of the following static methods.

OMPromise *promise = [OMPromise promiseWithResult:@1337];
// promise.state == OMPromiseStateFulfilled
// promise.result == @1337

OMPromise *promise1SecLate = [OMPromise promiseWithResult:@1338 after:1.f];
// promise1SecLate.state == OMPromiseStateUnfulfilled
// promise1SecLate.result == nil
// ... after 1 second ..
// promise1SecLate.state == OMPromiseStateFulfilled
// promise1SecLate.result == @1338

OMPromise *failed = [OMPromise promiseWithError:[NSError ...]];
// failed.state == OMPromiseStateFailed
// failed.error == [NSError ...]

// To delay the fail use promiseWithError:after: similar to promiseWithResult:after:

One special property of promises is, that they make only one state transition from unfulfilled to either failed or fulfilled. The promise itself doesn't provide an interface to do such transitions. That's why a superior class, called OMDeferred, exists, which yields a promise and is the only authority that might change the state of the aligned promise.

Thus the promises used in the above example are read-only. To create promises you might actually change you have to create a deferred, which you should keep to yourself, and return the promise aligned to the newly created deferred. To keep the promise informed you may use progress: multiple times, followed by at most one call of either fulfil: or fail: on the deferred.

- (OMPromise *)workIntensiveButSynchronousMethod {
    OMDeferred *deferred = [OMDeferred new];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // do your long-running task, eventually provide information about the progress..
        while (running)
            [deferred progress:progress];

        if (failed) {
            [deferred fail:error];
        } else {
            [deferred fulfil:result];
        }
    });

    return deferred.promise;
}

If you don't need the progress you could also simplify above snippet like this:

- (OMPromise *)workIntensiveButSynchronousMethod {
    return [OMPromise promiseWithTask:^{
        // do the long running task
        // ...

        // once we are done, we return the result, an NSError is automatically
        // treated as failure
        return (failed) ? error : result;
    } on:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
}

Callbacks - Reacting on changes in state

Once you have an instance of an OMPromise you might want to act on state changes. This is done by registering blocks using the methods fulfilled:, failed: and/or progressed: which are called if the corresponding event happens. The methods return self to simply chain multiple method calls and allow to register multiple callbacks for the same event.

[[OMPromise promiseWithResult:@1337 after:1.f]
    fulfilled:^(id result) {
        // called after 1 second, result == @1337
    }];

OMPromise *networkRequest = [OMHTTPRequest get:@"http://google.com"
                                    parameters:nil
                                       options:nil];
[[[networkRequest
    fulfilled:^(OMHTTPResponse *response) {
        // called if the network request succeeded
    }]
    failed:^(NSError *error) {
        // otherwise ...
    }]
    progressed:^(float progress) {
        // describes the progress as a value between 0.f and 1.f
    }];

Chaining - Creating promises depending on other promises

Sometimes it might be necessary to chain promises and to represent the chain by a promise itself. E.g. create small parts each described by a promise and use certain methods to combine these small parts to form a bigger picture.

The described behavior is achieved by using a method called then:. Similar to fulfilled: it takes a block that is executed once the promise gets fulfilled, but the block has to return a value. This value might be a new promise or any other object which is then bound to the newly created promise returned by then:. If any promise in the chain fails, the whole chain fails and all consecutive then-blocks are skipped.

// get User by its email
- (OMPromise *)getUserByEmail:(NSString *email);

// get all Comments created by a User
- (OMPromise *)getCommentsOfUser:(User *)user;

// get all Comment messages by a User having a certain email address
- (OMPromise *)getCommentsByUsersEmail:(NSString *)email {
    OMPromise *chain = [[[self getUserByEmail:email]
        then:^id(User *user) {
            // we can chain by returning other promises
            return [self getCommentsOfUser:user];
        }]
        then:^id(NSArray *comments) {
            // but also any other object to perform e.g. value transformations
            NSMutableArray *commentMessages = [NSMutableArray array];
            for (Comment *comment in comments) {
                [commentMessages addObject:comment.message];
            }
            return commentMessages;
        }];

    // chain fails if either the first promise, returned by getUserByEmail:,
    // or the second once, returned by getCommentsOfUser:, fails;
    // otherwise the results are propagated from promise to promise until the
    // last promise, representing the chain, is fulfilled
    return chain;
}

In certain cases it might be required to recover from a failure and let the chain continue as if everything happened correctly. That's when rescue: comes into play. Similar to then: it returns a newly created promise, but the supplied block is called if the promise fails.

// yields an UIImage if the user supplied an avatar
- (OMPromise *)getAvatarForUser:(User *)user;

// always returns an UIImage being either the User's supplied one or a dummy image
- (OMPromise *)getAvatarOrDummyForUser:(User *)user {
    OMPromise *chain = [[self getAvatarForUser:user]
        rescue:^id(NSError *error) {
            return [UIImage imageNamed:@"dummy_image.png"];
        }];

    // chain gets fulfilled always, due to rescue:
    return chain;
}

At that point some people might recognize the resemblance to monads in category theory. If not, you might benefit from looking into Haskell and its concepts.

Cancellation - Aborting an ongoing operation

If your abstracted operation is kinda heavy and long-running, you might want to support cancellation. It allows the owner of the promise to cancel the promise and thus the long-running operation, if possible. Once a promise is cancelled, it switches into the failed state with an cancellation specific error code.

A promise is by default not cancellable, but the owner of the deferred can make it so by registering at least one cancel-handler using cancelled:. It depends on the task and the use-case whether it makes sense to implement the cancel-handler.

- (OMPromise *)get100GofData {
    OMDeferred *deferred = [OMDeferred new];
    [deferred cancelled:^(OMDeferred *this) {
        // cancel the download..
    }];
    return deferred.promise;
}

- (void)startDownloadAndAbort {
    OMPromise *dl = [self get100GofData];
    // dl.cancellable == YES

    // ...
    // for an unknown reason we dont need it anymore
    [dl cancel];

    // if cancelled, it switches into the failed state
    [dl failed:^(NSError *error) {
        // error.domain == OMPromisesErrorDomain
        // error.code == OMPromisesCancelledError
    }];
}

Combinators & Transformers - Forming promise combinations besides chains

Currently only the following combinators are available, have a look at the documentation for a more detailed explanation. Each combinator creates a reasonable progress combination assuming an equal distribution of workload over all supplied promises.

  • join - Remove one level of promise wrapping
  • chain:initial: - Equal to applying multiple chaining/callback calls
  • all: - Waits for all promises to get fulfilled, fails in case any promise fails.
  • any: - Gets fulfilled if any one of the supplied promises does, otherwise it fails.
  • collect: - Collects all outcomes of the supplied promises, thus it never fails.
  • relay: - Relay all promise events to another deferred.

License

OMPromises is licensed under the terms of the MIT license. Please see the LICENSE file for full details.