Convert Figma logo to code with AI

Thomvis logoBrightFutures

Write great asynchronous code in Swift using futures and promises

1,901
184
1,901
0

Top Related Projects

24,358

Reactive Programming in Swift

Promises for Swift & ObjC.

40,982

Elegant HTTP Networking in Swift

Cocoa framework and Obj-C dynamism bindings for ReactiveSwift.

15,122

Network abstraction layer written in Swift.

Quick Overview

BrightFutures is a Swift library that provides a set of tools for working with asynchronous code, including futures, promises, and functional reactive programming. It aims to simplify the handling of asynchronous tasks and make it easier to write clean, composable, and testable code.

Pros

  • Simplifies Asynchronous Code: BrightFutures provides a clear and consistent API for working with asynchronous tasks, making it easier to write and reason about asynchronous code.
  • Supports Functional Reactive Programming: BrightFutures integrates well with functional reactive programming (FRP) libraries like RxSwift, allowing developers to combine asynchronous tasks with FRP concepts.
  • Comprehensive Feature Set: BrightFutures includes a wide range of features, including futures, promises, signals, and combinators, making it a versatile tool for handling various asynchronous use cases.
  • Cross-Platform Compatibility: BrightFutures is compatible with iOS, macOS, tvOS, and watchOS, allowing developers to use the same library across multiple Apple platforms.

Cons

  • Learning Curve: The library's comprehensive feature set and integration with FRP concepts may have a steeper learning curve for developers who are new to asynchronous programming or functional programming.
  • Dependency on Other Libraries: BrightFutures relies on other libraries, such as Result and Nimble, which may add complexity to the project setup and management.
  • Potential Performance Overhead: The abstraction and flexibility provided by BrightFutures may come with a slight performance overhead compared to lower-level asynchronous programming techniques.
  • Limited Documentation: While the library has good documentation, some developers may find it lacking in certain areas, especially for more advanced use cases.

Code Examples

Here are a few examples of how to use BrightFutures:

  1. Chaining Asynchronous Tasks:
let future1 = Future<Int, NoError> { fulfill in
    fulfill(42)
}

let future2 = future1.map { value in
    return value * 2
}

let future3 = future2.flatMap { value in
    return Future<String, NoError> { fulfill in
        fulfill("The answer is \(value)")
    }
}

future3.onSuccess { result in
    print(result) // Prints "The answer is 84"
}
  1. Handling Errors:
enum MyError: Error {
    case somethingWentWrong
}

let future = Future<Int, MyError> { fulfill, reject in
    reject(MyError.somethingWentWrong)
}

future.onFailure { error in
    print("Error: \(error)") // Prints "Error: somethingWentWrong"
}
  1. Combining Futures:
let future1 = Future<Int, NoError> { fulfill in
    fulfill(42)
}

let future2 = Future<String, NoError> { fulfill in
    fulfill("The answer is")
}

let combined = when(fulfilled: future1, future2).map { (value1, value2) in
    return "\(value2) \(value1)"
}

combined.onSuccess { result in
    print(result) // Prints "The answer is 42"
}
  1. Integrating with RxSwift:
import RxSwift

let future = Future<Int, NoError> { fulfill in
    fulfill(42)
}

let observable = future.asObservable()
observable.subscribe(onNext: { value in
    print("Received value: \(value)") // Prints "Received value: 42"
})

Getting Started

To get started with BrightFutures, you can follow these steps:

  1. Add the BrightFutures library to your project using a dependency manager like CocoaPods or Carthage.

    # CocoaPods
    pod 'BrightFutures'
    
    # Carthage
    github "Thomvis/BrightFutures"
    

Competitor Comparisons

24,358

Reactive Programming in Swift

Pros of RxSwift

  • More comprehensive and feature-rich, offering a wider range of operators and functionalities
  • Better suited for complex asynchronous operations and event-driven programming
  • Larger community and ecosystem, with more resources and third-party libraries available

Cons of RxSwift

  • Steeper learning curve due to its more extensive API and concepts
  • Can lead to more verbose code for simple use cases
  • Potential for overuse, which may result in unnecessary complexity in smaller projects

Code Comparison

BrightFutures:

let future = Future<String, NoError> { complete in
    DispatchQueue.main.async {
        complete(.success("Hello, World!"))
    }
}

RxSwift:

let observable = Observable<String>.create { observer in
    DispatchQueue.main.async {
        observer.onNext("Hello, World!")
        observer.onCompleted()
    }
    return Disposables.create()
}

Both libraries provide ways to handle asynchronous operations, but RxSwift offers a more extensive set of tools for managing complex event streams and reactive programming paradigms. BrightFutures focuses on simpler future/promise patterns, making it easier to learn and use for basic asynchronous tasks. The choice between the two depends on the project's complexity and specific requirements.

Promises for Swift & ObjC.

Pros of PromiseKit

  • More extensive documentation and examples
  • Wider adoption and community support
  • Better integration with Apple's frameworks and APIs

Cons of PromiseKit

  • Steeper learning curve for beginners
  • Slightly more verbose syntax in some cases
  • Larger codebase and potential overhead

Code Comparison

PromiseKit:

firstly {
    fetchUser()
}.then { user in
    fetchAvatar(for: user)
}.done { avatar in
    self.imageView.image = avatar
}.catch { error in
    print("Error: \(error)")
}

BrightFutures:

fetchUser().flatMap { user in
    fetchAvatar(for: user)
}.onSuccess { avatar in
    self.imageView.image = avatar
}.onFailure { error in
    print("Error: \(error)")
}

Both libraries provide similar functionality for handling asynchronous operations, but PromiseKit offers a more chainable syntax with firstly, then, and done. BrightFutures uses a more functional approach with flatMap and separate onSuccess and onFailure handlers. PromiseKit's syntax may be more intuitive for developers familiar with promise-based programming, while BrightFutures' approach might appeal to those comfortable with functional programming concepts.

40,982

Elegant HTTP Networking in Swift

Pros of Alamofire

  • More comprehensive networking library with additional features like request/response serialization and authentication
  • Larger community and more frequent updates
  • Better documentation and extensive examples

Cons of Alamofire

  • Heavier dependency with a larger codebase
  • Steeper learning curve for beginners
  • May include unnecessary features for simple networking tasks

Code Comparison

BrightFutures:

let future = Future<String, NoError> { complete in
    someAsyncOperation { result in
        complete(.success(result))
    }
}

Alamofire:

AF.request("https://api.example.com/data").responseJSON { response in
    switch response.result {
    case .success(let value):
        print("Success: \(value)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

BrightFutures focuses on providing a lightweight implementation of futures and promises, while Alamofire offers a more comprehensive networking solution. BrightFutures is ideal for developers who prefer a minimalistic approach to asynchronous programming, whereas Alamofire is better suited for projects requiring a full-featured networking library with additional capabilities.

Cocoa framework and Obj-C dynamism bindings for ReactiveSwift.

Pros of ReactiveCocoa

  • More comprehensive and feature-rich, offering a wider range of reactive programming concepts
  • Stronger community support and ecosystem with more third-party extensions
  • Better integration with Apple's Combine framework

Cons of ReactiveCocoa

  • Steeper learning curve due to its complexity and extensive API
  • Heavier dependency and potentially larger impact on app size
  • May be overkill for simpler projects or those with limited reactive programming needs

Code Comparison

ReactiveCocoa:

let searchResults = searchText
    .throttle(0.3, on: QueueScheduler.main)
    .flatMap(.latest) { (query: String) -> SignalProducer<[SearchResult], Error> in
        return API.search(query)
    }

BrightFutures:

let searchResults = searchText
    .debounce(0.3)
    .flatMap { query in
        API.search(query)
    }

Both libraries offer ways to handle asynchronous operations, but ReactiveCocoa provides a more extensive set of operators and a more reactive-oriented approach. BrightFutures focuses on simplicity and ease of use, making it more accessible for developers new to reactive programming or working on smaller projects.

While ReactiveCocoa offers more power and flexibility, BrightFutures may be sufficient for many use cases and can be easier to adopt in existing codebases. The choice between the two depends on the project's complexity, team expertise, and specific requirements.

15,122

Network abstraction layer written in Swift.

Pros of Moya

  • Specifically designed for network abstraction, making API integration simpler
  • Provides a more declarative approach to defining network requests
  • Offers built-in support for testing and stubbing network calls

Cons of Moya

  • Steeper learning curve due to its domain-specific language
  • May be overkill for simple networking tasks
  • Less flexible for general-purpose asynchronous programming

Code Comparison

Moya example:

let provider = MoyaProvider<MyService>()
provider.request(.userProfile) { result in
    switch result {
    case let .success(response):
        let data = response.data
        // Handle the response
    case let .failure(error):
        // Handle the error
    }
}

BrightFutures example:

let future = networkRequest()
future.onSuccess { response in
    let data = response.data
    // Handle the response
}.onFailure { error in
    // Handle the error
}

While Moya is tailored for network requests with a more declarative syntax, BrightFutures provides a more general-purpose approach to handling asynchronous operations. Moya's code is more specific to API calls, whereas BrightFutures can be used for various asynchronous tasks beyond networking.

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

BrightFutures

:warning: BrightFutures has reached end-of-life. After a long period of limited development activity, Swift's Async/Await has made the library obsolete. Please consider migrating from BrightFutures to async/await. When you do so, the async get() method will prove to be useful:

// in an async context...

let userFuture = User.logIn(username, password)
let user = try await userFuture.get()

// or simply:
let posts = try await Posts.fetchPosts(user).get()

The remainder of the README has not been updated recently, but is preserved for historic reasons.


How do you leverage the power of Swift to write great asynchronous code? BrightFutures is our answer.

BrightFutures implements proven functional concepts in Swift to provide a powerful alternative to completion blocks and support typesafe error handling in asynchronous code.

The goal of BrightFutures is to be the idiomatic Swift implementation of futures and promises. Our Big Hairy Audacious Goal (BHAG) is to be copy-pasted into the Swift standard library.

The stability of BrightFutures has been proven through extensive use in production. It is currently being used in several apps, with a combined total of almost 500k monthly active users. If you use BrightFutures in production, we'd love to hear about it!

Latest news

Join the chat at https://gitter.im/Thomvis/BrightFutures GitHub Workflow tests.yml status badge Carthage compatible CocoaPods version CocoaPods

BrightFutures 8.0 is now available! This update adds Swift 5 compatibility.

Installation

CocoaPods

  1. Add the following to your Podfile:

    pod 'BrightFutures'
    
  2. Integrate your dependencies using frameworks: add use_frameworks! to your Podfile.

  3. Run pod install.

Carthage

  1. Add the following to your Cartfile:

    github "Thomvis/BrightFutures"
    
  2. Run carthage update and follow the steps as described in Carthage's README.

Documentation

  • This README covers almost all features of BrightFutures
  • The tests contain (trivial) usage examples for every feature (97% test coverage)
  • The primary author, Thomas Visser, gave a talk at the April 2015 CocoaHeadsNL meetup
  • The Highstreet Watch App was an Open Source WatchKit app that made extensive use of an earlier version of BrightFutures

Examples

We write a lot of asynchronous code. Whether we're waiting for something to come in from the network or want to perform an expensive calculation off the main thread and then update the UI, we often do the 'fire and callback' dance. Here's a typical snippet of asynchronous code:

User.logIn(username, password) { user, error in
    if !error {
        Posts.fetchPosts(user, success: { posts in
            // do something with the user's posts
        }, failure: handleError)
    } else {
        handleError(error) // handeError is a custom function to handle errors
    }
}

Now let's see what BrightFutures can do for you:

User.logIn(username, password).flatMap { user in
    Posts.fetchPosts(user)
}.onSuccess { posts in
    // do something with the user's posts
}.onFailure { error in
    // either logging in or fetching posts failed
}

Both User.logIn and Posts.fetchPosts now immediately return a Future. A future can either fail with an error or succeed with a value, which can be anything from an Int to your custom struct, class or tuple. You can keep a future around and register for callbacks for when the future succeeds or fails at your convenience.

When the future returned from User.logIn fails, e.g. the username and password did not match, flatMap and onSuccess are skipped and onFailure is called with the error that occurred while logging in. If the login attempt succeeded, the resulting user object is passed to flatMap, which 'turns' the user into an array of his or her posts. If the posts could not be fetched, onSuccess is skipped and onFailure is called with the error that occurred when fetching the posts. If the posts could be fetched successfully, onSuccess is called with the user's posts.

This is just the tip of the proverbial iceberg. A lot more examples and techniques can be found in this readme, by browsing through the tests or by checking out the official companion framework FutureProofing.

Wrapping expressions

If you already have a function (or really any expression) that you just want to execute asynchronously and have a Future to represent its result, you can easily wrap it in an asyncValue block:

DispatchQueue.global().asyncValue {
    fibonacci(50)
}.onSuccess { num in
    // value is 12586269025
}

asyncValue is defined in an extension on GCD's DispatchQueue. While this is really short and simple, it is equally limited. In many cases, you will need a way to indicate that the task failed. To do this, instead of returning the value, you can return a Result. Results can indicate either a success or a failure:

enum ReadmeError: Error {
    case RequestFailed, TimeServiceError
}

let f = DispatchQueue.global().asyncResult { () -> Result<Date, ReadmeError> in
    if let now = serverTime() {
        return .success(now)
    }
    
    return .failure(ReadmeError.TimeServiceError)
}

f.onSuccess { value in
    // value will the NSDate from the server
}

The future block needs an explicit type because the Swift compiler is not able to deduce the type of multi-statement blocks.

Instead of wrapping existing expressions, it is often a better idea to use a Future as the return type of a method so all call sites can benefit. This is explained in the next section.

Providing Futures

Now let's assume the role of an API author who wants to use BrightFutures. A Future is designed to be read-only, except for the site where the Future is created. This is achieved via an initialiser on Future that takes a closure, the completion scope, in which you can complete the Future. The completion scope has one parameter that is also a closure which is invoked to set the value (or error) in the Future.

func asyncCalculation() -> Future<String, Never> {
    return Future { complete in
        DispatchQueue.global().async {
            // do a complicated task and then hand the result to the promise:
            complete(.success("forty-two"))
        }
    }
}

Never indicates that the Future cannot fail. This is guaranteed by the type system, since Never has no initializers. As an alternative to the completion scope, you could also create a Promise, which is the writeable equivalent of a Future, and store it somewhere for later use.

Callbacks

You can be informed of the result of a Future by registering callbacks: onComplete, onSuccess and onFailure. The order in which the callbacks are executed upon completion of the future is not guaranteed, but it is guaranteed that the callbacks are executed serially. It is not safe to add a new callback from within a callback of the same future.

Chaining callbacks

Using the andThen function on a Future, the order of callbacks can be explicitly defined. The closure passed to andThen is meant to perform side-effects and does not influence the result. andThen returns a new Future with the same result as this future that completes after the closure has been executed.

var answer = 10
    
let _ = Future<Int, Never>(value: 4).andThen { result in
    switch result {
    case .success(let val):
        answer *= val
    case .failure(_):
        break
    }
}.andThen { result in
    if case .success(_) = result {
        answer += 2
    }
}

// answer will be 42 (not 48)

Functional Composition

map

map returns a new Future that contains the error from this Future if this Future failed, or the return value from the given closure that was applied to the value of this Future.

fibonacciFuture(10).map { number -> String in
    if number > 5 {
        return "large"
    }
    return "small"
}.map { sizeString in
    sizeString == "large"
}.onSuccess { numberIsLarge in
    // numberIsLarge is true
}

flatMap

flatMap is used to map the result of a future to the value of a new Future.

fibonacciFuture(10).flatMap { number in
    fibonacciFuture(number)
}.onSuccess { largeNumber in
    // largeNumber is 139583862445
}

zip

let f = Future<Int, Never>(value: 1)
let f1 = Future<Int, Never>(value: 2)

f.zip(f1).onSuccess { a, b in
    // a is 1, b is 2
}

filter

Future<Int, Never>(value: 3)
    .filter { $0 > 5 }
    .onComplete { result in
        // failed with error NoSuchElementError
    }

Future<String, Never>(value: "Swift")
    .filter { $0.hasPrefix("Sw") }
    .onComplete { result in
        // succeeded with value "Swift"
    }

Recovering from errors

If a Future fails, use recover to offer a default or alternative value and continue the callback chain.

// imagine a request failed
Future<Int, ReadmeError>(error: .RequestFailed)
    .recover { _ in // provide an offline default
        return 5
    }.onSuccess { value in
        // value is 5 if the request failed or 10 if the request succeeded
    }

In addition to recover, recoverWith can be used to provide a Future that will provide the value to recover with.

Utility Functions

BrightFutures also comes with a number of utility functions that simplify working with multiple futures. These are implemented as free (i.e. global) functions to work around current limitations of Swift.

Fold

The built-in fold function allows you to turn a list of values into a single value by performing an operation on every element in the list that consumes it as it is added to the resulting value. A trivial usecase for fold would be to calculate the sum of a list of integers.

Folding a list of Futures is not very convenient with the built-in fold function, which is why BrightFutures provides one that works especially well for our use case. BrightFutures' fold turns a list of Futures into a single Future that contains the resulting value. This allows us to, for example, calculate the sum of the first 10 Future-wrapped elements of the fibonacci sequence:

let fibonacciSequence = [fibonacciFuture(1), fibonacciFuture(2),  ..., fibonacciFuture(10)]

// 1+1+2+3+5+8+13+21+34+55
fibonacciSequence.fold(0, f: { $0 + $1 }).onSuccess { sum in
    // sum is 143
}

Sequence

With sequence, you can turn a list of Futures into a single Future that contains a list of the results from those futures.

let fibonacciSequence = [fibonacciFuture(1), fibonacciFuture(2),  ..., fibonacciFuture(10)]
    
fibonacciSequence.sequence().onSuccess { fibNumbers in
    // fibNumbers is an array of Ints: [1, 1, 2, 3, etc.]
}

Traverse

traverse combines map and fold in one convenient function. traverse takes a list of values and a closure that takes a single value from that list and turns it into a Future. The result of traverse is a single Future containing an array of the values from the Futures returned by the given closure.

(1...10).traverse {
    i in fibonacciFuture(i)
}.onSuccess { fibNumbers in
    // fibNumbers is an array of Ints: [1, 1, 2, 3, etc.]
}

Delay

delay returns a new Future that will complete after waiting for the given interval with the result of the previous Future. To simplify working with DispatchTime and DispatchTimeInterval, we recommend to use this extension.

Future<Int, Never>(value: 3).delay(2.seconds).andThen { result in
    // execute after two additional seconds
}

Default Threading Model

BrightFutures tries its best to provide a simple and sensible default threading model. In theory, all threads are created equally and BrightFutures shouldn't care about which thread it is on. In practice however, the main thread is more equal than others, because it has a special place in our hearts and because you'll often want to be on it to do UI updates.

A lot of the methods on Future accept an optional execution context and a block, e.g. onSuccess, map, recover and many more. The block is executed (when the future is completed) in the given execution context, which in practice is a GCD queue. When the context is not explicitly provided, the following rules will be followed to determine the execution context that is used:

  • if the method is called from the main thread, the block is executed on the main queue
  • if the method is not called from the main thread, the block is executed on a global queue

If you want to have custom threading behavior, skip do do not the section. next :wink:

Custom execution contexts

The default threading behavior can be overridden by providing explicit execution contexts. You can choose from any of the built-in contexts or easily create your own. Default contexts include: any dispatch queue, any NSOperationQueue and the ImmediateExecutionContext for when you don't want to switch threads/queues.

let f = Future<Int, Never> { complete in
    DispatchQueue.global().async {
        complete(.success(fibonacci(10)))
    }
}

f.onComplete(DispatchQueue.main.context) { value in
    // update the UI, we're on the main thread
}

Even though the future is completed from the global queue, the completion closure will be called on the main queue.

Invalidation tokens

An invalidation token can be used to invalidate a callback, preventing it from being executed upon completion of the future. This is particularly useful in cases where the context in which a callback is executed changes often and quickly, e.g. in reusable views such as table views and collection view cells. An example of the latter:

class MyCell : UICollectionViewCell {
    var token = InvalidationToken()
    
    public override func prepareForReuse() {
        super.prepareForReuse()
        token.invalidate()
        token = InvalidationToken()
    }
    
    public func setModel(model: Model) {
        ImageLoader.loadImage(model.image).onSuccess(token.validContext) { [weak self] UIImage in
            self?.imageView.image = UIImage
        }
    }
}

By invalidating the token on every reuse, we prevent that the image of the previous model is set after the next model has been set.

Invalidation tokens do not cancel the task that the future represents. That is a different problem. With invalidation tokens, the result is merely ignored. Invalidating a token after the original future completed does nothing.

If you are looking for a way to cancel a running task, you could look into using NSProgress.

Credits

BrightFutures' primary author is Thomas Visser. He is lead iOS Engineer at Highstreet. We welcome any feedback and pull requests. Get your name on this list!

BrightFutures was inspired by Facebook's BFTasks, the Promises & Futures implementation in Scala and Max Howell's PromiseKit.

License

BrightFutures is available under the MIT license. See the LICENSE file for more info.