Swiftpack.co - Package - freshOS/Then

Then

Then

Language: Swift 5 Platform: iOS 8+/macOS10.11 SPM compatible Carthage compatible Cocoapods compatible Build Status codebeat badge License: MIT Release version

Reason - Example - Documentation - Installation

fetchUserId().then { id in
    print("UserID : \(id)")
}.onError { e in
    print("An error occured : \(e)")
}.finally {
    print("Everything is Done :)")
}
  let userId = try! await(fetchUserId())

Because async code is hard to write, hard to read, hard to reason about. A pain to maintain

Try it

then is part of freshOS iOS toolset. Try it in an example App! Download Starter Project

How

By using a then keyword that enables you to write aSync code that reads like an English sentence
Async code is now concise, flexible and maintainable ❤️

What

  • ☑ Based on the popular Promise / Future concept
  • Async / Await
  • progress race recover validate retry bridgeError chain noMatterWhat ...
  • ☑ Strongly Typed
  • ☑ Pure Swift & Lightweight

Example

Before

fetchUserId({ id in
    fetchUserNameFromId(id, success: { name in
        fetchUserFollowStatusFromName(name, success: { isFollowed in
            // The three calls in a row succeeded YAY!
            reloadList()
        }, failure: { error in
            // Fetching user ID failed
            reloadList()
        })
    }, failure: { error in
        // Fetching user name failed
        reloadList()
    })
}) {  error in
    // Fetching user follow status failed
    reloadList()
}
🙉🙈🙊#callbackHell

After

fetchUserId()
    .then(fetchUserNameFromId)
    .then(fetchUserFollowStatusFromName)
    .then(updateFollowStatus)
    .onError(showErrorPopup)
    .finally(reloadList)

🎉🎉🎉

Going further 🤓

fetchUserId().then { id in
    print("UserID : \(id)")
}.onError { e in
    print("An error occured : \(e)")
}.finally {
    print("Everything is Done :)")
}

If we want this to be maintainable, it should read like an English sentence
We can do this by extracting our blocks into separate functions:

fetchUserId()
    .then(printUserID)
    .onError(showErrorPopup)
    .finally(reloadList)

This is now concise, flexible, maintainable, and it reads like an English sentence <3
Mental sanity saved // #goodbyeCallbackHell

Documentation

  1. Writing your own Promise
  2. Progress
  3. Registering a block for later
  4. Returning a rejecting promise
  5. Common Helpers
  6. race
  7. recover
  8. validate
  9. retry
  10. bridgeError
  11. whenAll
  12. chain
  13. noMatterWhat
  14. unwrap
  15. AsyncTask
  16. Async/Await

Writing your own Promise 💪

Wondering what fetchUserId() is?
It is a simple function that returns a strongly typed promise :

func fetchUserId() -> Promise<Int> {
    return Promise { resolve, reject in
        print("fetching user Id ...")
        wait { resolve(1234) }
    }
}

Here you would typically replace the dummy wait function by your network request <3

Progress

As for then and onError, you can also call a progress block for things like uploading an avatar for example.

uploadAvatar().progress { p in
  // Here update progressView for example
}
.then(doSomething)
.onError(showErrorPopup)
.finally(doSomething)

Registering a block for later

Our implementation slightly differs from the original javascript Promises. Indeed, they do not start right away, on purpose. Calling then, onError, or finally will start them automatically.

Calling then starts a promise if it is not already started. In some cases, we only want to register some code for later. For instance, in the case of JSON to Swift model parsing, we often want to attach parsing blocks to JSON promises, but without starting them.

In order to do that we need to use registerThen instead. It's the exact same thing as then without starting the promise right away.

let fetchUsers:Promise<[User]> = fetchUsersJSON().registerThen(parseUsersJSON)

// Here promise is not launched yet \o/

// later...
fetchUsers.then { users in
    // YAY
}

Note that onError and finally also have their non-starting counterparts : registerOnError and registerFinally.

Returning a rejecting promise

Oftetimes we need to return a rejecting promise as such :

return Promise { _, reject in
  reject(anError)
}

This can be written with the following shortcut :

return Promise.reject(error:anError)

Common Helpers

Race

With race, you can send multiple tasks and get the result of the first one coming back :

race(task1, task2, task3).then { work in
  // The first result !
}

Recover

With .recover, you can provide a fallback value for a failed Promise.
You can :

  • Recover with a value
  • Recover with a value for a specific Error type
  • Return a value from a block, enabling you to test the type of error and return distinct values.
  • Recover with another Promise with the same Type
.recover(with: 12)
.recover(MyError.defaultError, with: 12)
.recover { e in
  if e == x { return 32 }
  if e == y { return 143 }
  throw MyError.defaultError
}
.recover { e -> Promise<Int> in
  // Deal with the error then
  return Promise<Int>.resolve(56)
  // Or
  return Promise<Int>.reject(e)
  }
}
.recover(with: Promise<Int>.resolve(56))

Note that in the block version you can also throw your own error \o/

Validate

With .validate, you can break the promise chain with an assertion block.

You can:

  • Insert assertion in Promise chain
  • Insert assertion and return you own Error

For instance checking if a user is allowed to drink alcohol :

fetchUserAge()
.validate { $0 > 18 }
.then { age in
  // Offer a drink
}

.validate(withError: MyError.defaultError, { $0 > 18 })`

A failed validation will retrun a PromiseError.validationFailed by default.

Retry

With retry, you can restart a failed Promise X number of times.

doSomething()
  .retry(10)
  .then { v in
   // YAY!
  }.onError { e in
    // Failed 10 times in a row
  }

BridgeError

With .bridgeError, you can intercept a low-level Error and return your own high level error. The classic use-case is when you receive an api error and you bridge it to your own domain error.

You can:

  • Catch all errors and use your own Error type
  • Catch only a specific error
.bridgeError(to: MyError.defaultError)
.bridgeError(SomeError, to: MyError.defaultError)

WhenAll

With .whenAll, you can combine multiple calls and get all the results when all the promises are fulfilled :

whenAll(fetchUsersA(),fetchUsersB(), fetchUsersC()).then { allUsers in
  // All the promises came back
}

Chain

With chain, you can add behaviours without changing the chain of Promises.

A common use-case is for adding Analytics tracking like so:

extension Photo {
    public func post() -> Async<Photo> {
        return api.post(self).chain { _ in
            Tracker.trackEvent(.postPicture)
        }
    }
}

NoMatterWhat

With noMatterWhat you can add code to be executed in the middle of a promise chain, no matter what happens.

func fetchNext() -> Promise<[T]> {
    isLoading = true
    call.params["page"] = page + 1
    return call.fetch()
        .registerThen(parseResponse)
        .resolveOnMainThread()
        .noMatterWhat {
            self.isLoading = false
    }
}

Unwrap

With unwrap you can transform an optional into a promise :

func fetch(userId: String?) -> Promise<Void> {
   return unwrap(userId).then {
        network.get("/user/\($0)")
    }
}

Unwrap will fail the promise chain with unwrappingFailed error in case of a nil value :)

AsyncTask

AsyncTask and Async<T> typealisases are provided for those of us who think that Async can be clearer than Promise. Feel free to replace Promise<Void> by AsyncTask and Promise<T> by Async<T> wherever needed.
This is purely for the eyes :)

Async/Await

await waits for a promise to complete synchronously and yields the result :

let photos = try! await(getPhotos())

async takes a block and wraps it in a background Promise.

async {
  let photos = try await(getPhotos())
}

Notice how we don't need the ! anymore because async will catch the errors.

Together, async/await enable us to write asynchronous code in a synchronous manner :

async {
  let userId = try await(fetchUserId())
  let userName = try await(fetchUserNameFromId(userId))
  let isFollowed = try await(fetchUserFollowStatusFromName(userName))
  return isFollowed
}.then { isFollowed in
  print(isFollowed)
}.onError { e in
  // handle errors
}

Await operators

Await comes with .. shorthand operator. The ..? will fallback to a nil value instead of throwing.

let userId = try await(fetchUserId())

Can be written like this:

let userId = try ..fetchUserId()

Installation

The Swift Package Manager (SPM) is now the official way to install Then. The other package managers are now deprecated as of 5.1.3 and won't be supported in future versions.

Swift Package Manager

Xcode > File > Swift Packages > Add Package Dependency... > Paste https://github.com/freshOS/Then

Cocoapods - Deprecated

target 'MyApp'
pod 'thenPromise'
use_frameworks!

Carthage - Deprecated

github "freshOS/then"

Contributors

S4cha, Max Konovalov, YannickDot, Damien, piterlouis

Swift Version

  • Swift 2 -> version 1.4.2
  • Swift 3 -> version 2.2.5
  • Swift 4 -> version 3.1.0
  • Swift 4.1 -> version 4.1.1
  • Swift 4.2 -> version 4.2.0
  • Swift 4.2.1 -> version 4.2.0
  • Swift 5.0 -> version 5.0.0
  • Swift 5.1 -> version 5.1.0
  • Swift 5.1.3 -> version 5.1.2

Backers

Like the project? Offer coffee or support us with a monthly donation and help us continue our activities :)

Sponsors

Become a sponsor and get your logo on our README on Github with a link to your site :)

Github

link
Stars: 872

Dependencies

Used By

Total: 0

Releases

SPM Official - 2020-01-02 10:08:18

The project now follows the Swift package Manager conventions which simplifies package maintenance. Swift Package Manager is now the official way to integrate Then on all platforms. 🎉 Onwards ! ✨🥂

Updates Package.swift - 2019-12-23 14:36:21

  • Updates Package.swift

Xcode 11.3 & Swift 5.1.3 - 2019-12-13 08:29:44

  • Build with Xcode 11.3 & Swift 5.1.3

watchOS support ⌚️🎉 - 2019-11-18 09:57:51

  • Adds watchOS support, Thanks to @jos76 👏✨

Xcode 11 & Swift 5.1 - 2019-09-27 09:58:34

  • Updates for Xcode 11 & Swift 5.1
  • Renames framework from then to Then to comply to standards. thanks @MoveUpwardsDev 👍

Xcode 10.2 & Swift 5 - 2019-04-24 20:45:01

Adds Swift 5 support courtesy of @bernardowilson 🎉

Xcode 10.1 & Swift 4.2.1 - 2018-12-19 14:41:08

Adds the Carthage pre-built framework for Xcode 10.1 & Swift 4.2.1

Xcode10 & Swift 4.2 - 2018-09-19 07:20:33

  • Migrates to Xcode10 & Swift 4.2 🚀

4.1.1 - 2018-06-22 09:17:28

  • Adds @discardableResult to await
  • Adds lazyWhenAll (courtesy of @cfanfs 👏 )
  • Resovles a data race on lockQueue (@cfans again 🚀 )

Thread Safety 🔒 - 2018-05-30 16:14:41

Thread Safety 🔒

  • Fixes WheAll race condition when using concurrent operations
  • Fixes overall thread safety
  • .. & ..? syntax for await

Big thanks @cfanfs for his work on this release 👏

Xcode 9.3 & Swift 4.1 - 2018-04-04 18:22:44

Big Thanks to @casademora for this release 👏 🎉

This introduces breaking changes.

Promises calls like this:

func foo() -> Promise<Void> {
    return Promise { resolve, _ in
      // code...
  }
}

Will now need to be disambiguated like so:

func foo() -> Promise<Void> {
    return Promise { (resolve: @escaping () -> Void, _) in
      // code...
  }
}

Indeed, Promise<T> and Promise<Void> share the same initializer with different param types which is now considered ambiguous. Until we find a workaround, explicitly declaring types will be needed.

3.1.0 - 2018-02-05 15:34:17

Kudos to @qhua948 for adding a cool recover overload 🎉

recover(_ block:@escaping (Error) throws -> Promise<T>) -> Promise<T>

Also made zip concurrent (faster!) and thread safe with up to 8 inputs :)

WhenAll DispatchQueue - 2018-01-03 10:49:59

  • whenAll now accepts an optional DispatchQueue

Kudos to @cliapis for finding this issue 👏

Happy New Year everybody! 🍾 🎉

Fixes SPM integration &remove test coverage data - 2017-09-27 13:18:45

Fixes SPM integration &remove test coverage data

Swift 4 & Xcode9 ! - 2017-09-25 08:25:10

Swift 4 & Xcode9 support.

tvOS Support - 2017-08-21 08:35:01

tvOS is now officially supported thanks to @florianbuerger ! Kudos 👏🎉

Fixes Linux build - 2017-07-27 12:27:11

  • Fixes : async/await use in Ubuntu 16.04 results in error: use of unresolved identifier 'DispatchGroup' Kudos to @crarau and @ykyouhei 👏

Solves retain cycles - 2017-05-05 07:52:58

Big kudos to @cartermike who found and fixed retain cycles ! 🎉🍻

Built with Xcode 8.3.1 - 2017-04-13 12:40:30

Unwrap! - 2017-03-20 09:14:34

Adds unwrap helper

Async/ Await! - 2017-03-16 13:08:06

  • async/ await
  • noMatterWhat
  • chain

Promise helpers! - 2017-03-01 06:53:18

This version Introduces common helpers to help compose your promise chains :

race to send multiple promises and get the first coming back .recover to provide a default value in case of error .bridgeError to intercept errors and pass your own .validate to check a condition .retry to relaunch the promise a certain number of times AsyncTask and Async<T> typealisases for those of use who think that Async is clearer than Promise.

Big thanks to @MontakOleg for his precious help on this version :)

Linux support - 2017-02-07 08:44:49

Linux support

2.0.2 - 2016-11-07 19:24:56

  • Adds Shortcut for returning a rejecting promise
  • Pre-built framework built with Xcode 8.1
  • Fixed finally Xcode auto indent issue
  • Code improvements

Fixes retain cycle - 2016-10-12 08:25:30

Big kudos to @mandarin6b0 who found a retain cycle and worked out the solution ! This is now fixed \o/

Swift 3 is here! - 2016-09-22 08:08:59

This is the first version packaging swift 3 and with a bonus, macOS support!

WhenAll now fails if one of the promises fails - 2016-08-06 09:26:07

Progress - 2016-07-31 18:04:04

  • progress is now available \o/
  • onError returns its own Promise
  • finally return its own Promise
  • We can now register multiple then / onError/ finally / progress blocks on the same promise.

RegisterThen - 2016-04-23 16:40:06

Registering a block for later

Calling then starts a promise if it is not already started. In some cases, we only want to register some code for later. For instance, in the case of JSON to Swift model parsing, we often want to attach parsing blocks to JSON promises, but without starting them.

In order to do that we need to use registerThen instead. It's the exact same thing as then without starting the promise right away.

let fetchUsers:Promise<[User]> = fetchUsersJSON().registerThen(parseUsersJSON)

// Here promise is not launched yet \o/

// later...
fetchUsers.then { users in
    // YAY
}

Faster Carthage updates - 2016-04-17 16:16:04

Adding a pre-built framework for faster Carthage updates :)