• This repository has been archived on 06/Feb/2023
  • Stars
    star
    317
  • Rank 132,216 (Top 3 %)
  • Language
    Swift
  • License
    MIT License
  • Created over 8 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Streamlined Future<Value, Error> implementation

Streamlined Future<Value, Error> implementation


Future represents a result of a task which may be available now, or in the future, or never. Future provides a streamlined Future<Value, Error> type engineered with ergonomics and performance in mind.

Futures enable composition of tasks using familiar functions like map, flatMap, zip, reduce and others which are all easy to learn and use.

Getting Started

To learn more see a full API Reference. When you are ready to install, follow the Installation Guide. See Requirements for a list of supported platforms.

Quick Start Guide

Let's start with an overview of the available types. The central ones is, of course, Future:

struct Future<Value, Error> {
    public typealias Result = Swift.Result<Value, Error>
    var result: Result? { get }
    
    func on(success: ((Value) -> Void)?, failure: ((Error) -> Void)?, completion: (() -> Void)?)
}

Future is parameterized with two generic arguments – Value and Error. This allows us to take advantage of Swift type-safety features and also model futures that never fail using Never – Future<Value, Never>.

Create Future

To create a future you would normally use a Promise:

func someAsyncTask() -> Future<Value, Error> {
    let promise = Promise<Value, Error>()
    performAsyncTask { value, error in
        // If success
        promise.succeed(value: value)
        // If error
        promise.fail(error: error)
    }
    return promise.future
}

Promise is thread-safe. You can call succeed or fail from any thread and any number of times – only the first result is sent to the Future.

If the result of the work is already available by the time the future is created use one of these initializers:

// Init with a value
Future(value: 1) // Inferred to be Future<Int, Never>
Future<Int, MyError>(value: 1)

// Init with an error
Future<Int, MyError>(error: .dataCorrupted)

// Init with a throwing closure
Future<Int, Error> {
    guard let value = Int(string) else {
        throw Error.dataCorrupted
    }
    return value
}

These init methods require no allocations which makes them really fast, faster than allocation a Promise instance.

Attach Callbacks

To attach callbacks (each one is optional) to the Future use on method:

let future: Future<Value, Error>
future.on(success: { print("received value: \($0)") },
          failure: { print("failed with error: \($0)") }),
          completion: { print("completed") })

If the future already has a result, callbacks are executed immediately. If the future doesn't have a result yet, callbacks will be executed when the future is resolved. The future guarantees that it can be resolved with only one result, the callbacks are also guaranteed to run only once.

By default, the callbacks are run on the .main scheduler. If the task finishes on the main thread, the callbacks are executed immediately. Otherwise, they are dispatched to be executed asynchronously on the main thread.

See Threading for a rationale and more info.

wait

Use wait method to block the current thread and wait until the future receives a result:

let result = future.wait() // Mostly useful for testing and debugging

result

If the future already has a result you can read it synchronously:

struct Future<Value, Error> {
    var value: Value? { get }
    var error: Error? { get }
    var result: Result? { get }
}

Composition

map, flatMap

Use familiar map and flatMap function to transform the future's values and chain futures:

let user: Future<User, Error>
func loadAvatar(url: URL) -> Future<UIImage, Error>

let avatar = user
    .map { $0.avatarURL }
    .flatMap(loadAvatar)

If you are not familiar with flatMap, at first it might be hard to wrap your head around it. But when it clicks, using it becomes second nature.

There is actually not one, but a few flatMap variations. The extra ones allow you to seamlessly mix futures that can produce an error and the ones that can't.

mapError, flatMapError

Future has typed errors. To convert from one error type to another use mapError:

let request: Future<Data, URLError>
request.mapError(MyError.init(urlError:))

Use flatMapError to "recover" from an error.

If you have a future that never produces an error (Future<_, Never>) you can cast it to the future which can produce any error using castError method. In most cases, this is not needed though.

zip

Use zip to combine the result of up to three futures in a single future:

let user: Future<User, Error>
let avatar: Future<UIImage, Error>

Future.zip(user, avatar).on(success: { user, avatar in
    // use both values
})

Or to wait for the result of multiple futures:

Future.zip([future1, future2]).on(success: { values in
    // use an array of values
})

reduce

Use reduce to combine the results of multiple futures:

let future1 = Future(value: 1)
let future2 = Future(value: 2)

Future.reduce(0, [future1, future2], +).on(success: { value in
    print(value) // prints "3"
})

Extensions

In addition to the primary interface, there is also a set of extensions to Future which includes multiple convenience functions. Not all of them are mentioned here, look into FutureExtensions.swift to find more!

first

Use first to wait for a first future to succeed:

let requests: [Future<Value, Error>]
Future.first(requests).on(success: { print("got response!") })

forEach

Use forEach to perform the work in a sequence:

// `startWork` is a function that returns a future
Future.forEach([startWork, startOtherWork]) { future in
    // In the callback you can subscribe to each future when work is started
    future.on(success: { print("work is completed") })
}

after

Use after to produce a value after a given time interval.

Future.after(seconds: 2.5).on { print("2.5 seconds passed") })

retry

Use retry to perform the given number of attempts to finish the work successfully.

func startSomeWork() -> Future<Value, Error>

Future.retry(attempts: 3, delay: .seconds(3), startSomeWork)

Retry is flexible. It allows you to specify multiple delay strategies including exponential backoff, to inspect the error before retrying and more.

materialize

This one is fascinating. It converts Future<Value, Error> to Future<Future<Value, Error>.Result, Never> – a future which never fails. It always succeeds with the result of the initial future. Now, why would you want to do that? Turns out materialize composes really well with other functions like zip, reduce, first, etc. All of these functions fail as soon as one of the given futures fails. But with materialize you can change the behavior of these functions so that they would wait until all futures are resolved, no matter successfully or with an error.

Notice that we use native Never type to represent a situation when error can never be produced.

Future.zip(futures.map { $0.materialize() }).on { results in
    // All futures are resolved and we get the list of all of the results -
    // either values or errors.
}

Threading

On iOS users expect UI renders to happen synchronously. To accommodate that, by default, the callbacks are run using Scheduler.main. It runs work immediately if on the main thread, otherwise asynchronously on the main thread. The design is similar to the reactive frameworks like RxSwift. It opens a whole new area for using futures which are traditionally asynchronous by design.

There are three schedulers available:

enum Scheduler {
    /// If the task finishes on the main thread, the callbacks are executed
    /// immediately. Otherwise, they are dispatched to be executed
    /// asynchronously on the main thread.
    static var main: ScheduleWork

    /// Immediately executes the given closure.
    static var immediate: ScheduleWork

    /// Runs asynchronously on the given queue.
    static func async(on queue: DispatchQueue, flags: DispatchWorkItemFlags = []) -> ScheduleWork
}

ScheduleWork is just a function so you can easily provide a custom implementation.

To change the scheduler on which callbacks are called use observe(on:):

// There are two variants, one with `DispatchQueue`, one with `Scheduler`.
// Here's the one with `DispatchQueue`:
future.observe(on: .global())
    on(success: { print("value: \($0)" })

You can also use observe(on:) to perform transformations like map, tryMap and others on background queues:

future.observe(on: .global())
    .map { /* heavy operation */ }

Please keep in mind that only the future returned directly by observe(on:) is guaranteed to run its continuations on the given queue (or scheduler).

Cancellation

Cancellation is a concern orthogonal to Future. Think about Future as a simple callback replacement – callbacks don't support cancellation.

Future implements a CancellationToken pattern for cooperative cancellation of async tasks. A token is created through a cancellation token source.

let cts = CancellationTokenSource()
asyncWork(token: cts.token).on(success: {
    // To prevent closure from running when task is cancelled use `isCancelling`:
    guard !cts.isCancelling else { return }
    
    // Do something with the result
}) 

// At some point later, can be on the other thread:
cts.cancel()

To cancel multiple async tasks, you can pass the same token to all of them.

Implementing async tasks that support cancellation is easy:

func loadData(with url: URL, _ token: CancellationToken = .none) -> Future<Data, URLError> {
    let promise = Promise<Data, URLError>()
    let task = URLSession.shared.dataTask(with: url) { data, error in
        // Handle response
    }
    token.register(task.cancel)
    return promise.future
}

The task has full control over cancellation. You can ignore it, you can fail a promise with a specific error, return a partial result, or not resolve a promise at all.

CancellationTokenSource itself is built using Future and benefits from all of its performance optimizations.

Async/Await

Async/await is often built on top of futures. When async/await support is eventually added to Swift, it would be relatively easy to replace the code that uses futures with async/await.

There is a (blocking) version of async/await built on top Future. It's not meant to be used in production.

Performance

Every feature in Future is engineered with performance in mind.

We avoid dynamic dispatch, reduce the number of allocations and deallocations, avoid doing unnecessary work and lock as less as possible. Methods are often implemented in a sometimes less elegant but more performant way.

There are also some key design differences that give Future an edge over other frameworks. One example is Future type itself which is designed as struct which allows some common operations to be performed without a single allocation.

Requirements

Future Swift Xcode Platforms
Future 1.4 Swift 5.1 Xcode 11 iOS 11.0 / watchOS 4.0 / macOS 10.13 / tvOS 11.0 / Linux
Future 1.1 Swift 5.0 Xcode 10.2 iOS 10.0 / watchOS 3.0 / macOS 10.12 / tvOS 10.0
Future 1.0 Swift 4.2 Xcode 10.1 iOS 10.0 / watchOS 3.0 / macOS 10.12 / tvOS 10.0
Future 0.17 Swift 4.0 Xcode 9.2 iOS 9.0 / watchOS 2.0 / macOS 10.11 / tvOS 9.0

License

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

More Repositories

1

Nuke

Image loading system
Swift
8,112
star
2

Pulse

Network logger for Apple platforms
Swift
6,307
star
3

DFImageManager

Image loading, processing, caching and preheating
Objective-C
1,180
star
4

Get

Web API client built using async/await
Swift
941
star
5

Preheat

Automates prefetching of content in UITableView and UICollectionView
Swift
629
star
6

PulsePro

A macOS app for viewing logs from Pulse
Swift
482
star
7

Align

Intuitive and powerful Auto Layout library
Swift
350
star
8

FetchImage

Makes it easy to download images using Nuke and display them in SwiftUI apps
Swift
212
star
9

Regex

Open source regex engine
Swift
211
star
10

Arranged

Open source replacement of UIStackView for iOS 8 (100% layouts supported)
Swift
208
star
11

VPN

Sample custom VPN client/server in Swift
Swift
182
star
12

Formatting

Swift
179
star
13

DFCache

Composite LRU cache with fast metadata using UNIX extended file attributes
Objective-C
162
star
14

RxNuke

RxSwift extensions for Nuke
Swift
148
star
15

CreateAPI

Delightful code generator for OpenAPI specs
Swift
142
star
16

SwiftSQL

Swift API for SQLite
Swift
131
star
17

ThreeColumnNavigation

A minimal example of three-column navigation for iPad and macOS using SwiftUI
Swift
127
star
18

Stacks

A micro UIStackView convenience API inspired by SwiftUI
Swift
73
star
19

Nuke-FLAnimatedImage-Plugin

FLAnimatedImage plugin for Nuke
Swift
53
star
20

RxUI

Auto-binding for RxSwift inspired by SwiftUI
Swift
45
star
21

Nuke-Alamofire-Plugin

Alamofire plugin for Nuke
Swift
40
star
22

NukeDemo

Nuke Demo
Swift
34
star
23

DFJPEGTurbo

Objective-C libjpeg-turbo wrapper
C
33
star
24

ImagePublisher

Combine publishers for Nuke
Swift
25
star
25

NukeUI

Lazy image loading for Apple platforms: SwiftUI, UIKit, AppKit
Swift
19
star
26

articles

Articles for kean.github.io
19
star
27

URLQueryEncoder

URL query encoder with support for all OpenAPI serialization options
Swift
17
star
28

NukeBuilder

A fun and convenient way to use Nuke
Swift
14
star
29

ScrollViewPrefetcher

Prefetching for SwiftUI
Swift
14
star
30

PulseLogHandler

SwiftLog Extension for Pulse
Swift
12
star
31

HTTPHeaders

Parsing Simple HTTP Headers
Swift
11
star
32

OctoKit

GitHub API client built with Fuse
Swift
8
star
33

PulseApps

Base Pulse macOS and iOS apps and a few demo projects
Swift
7
star
34

kean

1
star