Swift Package Index


Promises with async/await, suspend/resume and cancel features for Swift.


  • The latest stable release is 1.4.0. Released 6 months ago.
  • The last commit to master was 6 months ago.

Swift Version Compatibility

  • 1.4.0 and master
    5.3
    5.2
    5.1
    5.0
    4.2
Full build results

Platform Compatibility

  • 1.4.0 and master
    iOS
    macOS(Intel)
    macOS(ARM)
    Linux
    tvOS
    watchOS
Full build results

PromiseQ

Language: Swift Platform: iOS 8+/macOS10.11 SPM compatible Build Status codecov

Fast, powerful and lightweight implementation of Promises for Swift.

Features

High-performance

Promises closures are called synchronously one by one if they are on the same queue and asynchronous otherwise.

Lightweight

Whole implementation consists on several hundred lines of code.

Memory safe

PromiseQ is based on struct and a stack of callbacks that removes many problems of memory management such as reference cycles etc.

Standard API

Based on JavaScript Promises/A+ spec, supports async/await and it also includes standard methods: Promise.all, Promise.race, Promise.resolve/reject.

Suspension

It is an additional useful feature to suspend the execution of promises and resume them later. Suspension does not affect the execution of a promise that has already begun it stops execution of next promises.

Cancelation

It is possible to cancel all queued promises at all in case to stop an asynchronous logic. Cancellation does not affect the execution of a promise that has already begun it cancels execution of the next promises.

Basic Usage

Promise

Promise is a generic type that represents an asynchronous operation and you can create it in a simple way with a closure e.g.:

Promise {
	try String(contentsOfFile: file)
}

The provided closure is called asynchronously after the promise is created. By default the closure runs on the global default queue DispatchQueue.global() but you can also specify a needed queue to run:

Promise(.main) {
	self.label.text = try String(contentsOfFile: file) // Runs on the main queue
}

The promise can be resolved when the closure returns a value or rejected when the closure throws an error.

Also the closure can settle the promise with resolve/reject callbacks for asynchronous tasks:

Promise { resolve, reject in
	// Will be resolved after 2 secs
	DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
		resolve("done")
	}
}

then

It takes a provided closure and returns new promise. The closure runs when the current promise is resolved, and receives the result.

Promise {
	try String(contentsOfFile: "file.txt")
}
.then { text in
	print(text)
}

In this way we can pass results through the chain of promises:

Promise {
	try String(contentsOfFile: "file.txt")
}
.then { text in
	return text.count
}
.then { count in
	print(count)
}

Also the closure can return a promise and it will be injected in the promise chain:


Promise {
	return 200
}
.then { value in
	Promise {
		value / 10
	}
}
.then { value in
	print(value)
}
// Prints "20"

catch

It takes a closure and return a new promise. The closure runs when the promise is rejected, and receives the error.

Promise {
	try String(contentsOfFile: "nofile.txt") // Jumps to catch
}
.then { text in
	print(text) // Doesn't run
}
.catch { error in
	print(error.localizedDescription)
}
// Prints "The file `nofile.txt` couldn’t be opened because there is no such file."

finally

This always runs when the promise is settled: be it resolve or reject so it is a good handler for performing cleanup etc.

Promise {
	try String(contentsOfFile: "file.txt")
}
.finally {
	print("Finish reading") // Always runs
}
.then { text in
	print(text)
}
.catch { error in
	print(error.localizedDescription)
}
.finally {
	print("The end") // Always runs
}

Promise.resolve/reject

These are used for compatibility e.g. when it's simple needed to return a resolved or rejected promise.

Promise.resolve creates a resolved promise with a given value:

Promise {
	return 200
}

// Same as above
Promise.resolve(200)

Promise.reject creates a rejected promise with a given error:

Promise {
	throw error
}

// Same as above
Promise.reject(error)

Promise.all

It returns a promise that resolves when all listed promises from the provided list are resolved, and the array of their results becomes its result. If any of the promises is rejected, the promise returned by Promise.all immediately rejects with that error:

Promise.all(
    Promise {
        return "Hello"
    },
    Promise { resolve, reject in
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            resolve("World")
        }
    }
)
.then { results in
    print(results)
}
// Prints ["Hello", "World"]

You can set settled=true param to make a promise that resolves when all listed promises are settled regardless of their results:

Promise.all(settled: true,
    Promise<Any> { resolve, reject in
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            reject(error)
        }
    },
    Promise {
        return 200
    }
)
.then { results in
    print(results)
}
// Prints [error, 200]

Promise.race

It makes a promise that waits only for the first settled promise from the given list and gets its result (or error).

Promise.race(
	Promise { resolve, reject in
		DispatchQueue.main.asyncAfter(deadline: .now() + 2) { // Wait 2 secs
			reject("Error")
		}
	},
	Promise { resolve, reject in
		DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // Wait 1 sec
			resolve(200)
		}
	}
)
.then {
	print($0)
}
// Prints "200"

Advanced Usage

timeout

timeout parameter allows to wait for a promise for a time interval and reject it with PromiseError.timedOut error, if it doesn't resolve within the given time.

Promise(timeout: 10) { // Wait 10 secs for data
	try loadData()
}
.then(timeout: 1) { data in //  Wait 1 sec for parsed data
	try parse(data)
}
.catch(timeout: 1) { error in // Wait 1 sec to handle errors
	if case PromiseError.timedOut = error {
		print(error)
	}
	else {
		handleError(error)
	}
}

retry

retry parameter provides the ability to reattempt a task if the promise is rejected. By default, there is a single attempt to resolve the promise but you can increase the number of attempts with this parameter:

Promise(retry: 3) { // Makes 3 attempts to load data after the rejection
	try loadData()
}
.then { data in
	parse(data)
	...
}
.catch { error in
	print(error) // Calls if the `loadData` fails 4 times (1 + 3 retries)
}

async/await

It's a special notation to work with promises in a more comfortable way and it’s easy to understand and use.

async is a alias for Promise so you can use it to create a promise as well:

// Returns a promise with `String` type
func readFile(_ file: String) -> async<String> {
	return async {
		try String(contentsOfFile: file)
	}
}

await() is a function that synchronously waits for a result of the promise or throws an error otherwise.

let text = try readFile("file.txt").await()

To avoid blocking the current queue (such as main UI queue) we can pass await() inside the other promise (async block) and use catch to handle errors as usual:

async {
	let text = try readFile("file.txt").await()
	print(text)
}
.catch { error in
	print(error.localizedDescription)
}

suspend/resume

suspend() temporarily suspends a promise. Suspension does not affect the execution of the current promise that has already begun it stops execution of next promises in the chain. The promise can continue executing at a later time with resume().

let promise = Promise {
	String(contentsOfFile: file)
}
promise.suspend()
...
// Later
promise.resume()

cancel

Cancels execution of the promise. Cancelation does not affect the execution of the promise that has already begun it cancels execution of next promises in the chain.

let promise = Promise {
	String(contentsOfFile: file) // Never runs
}
.then { text in
	print(text) // Never runs
}
promise.cancel()

You can also break the promise chain for some conditions to call cancel inside a closure of any promise e.g.:

let promise = Promise {
	return getStatusCode()
}

promise.then { statusCode in
	guard statusCode == 200 else {
		promise.cancel() // Breaks the promise chain
		return
	}
	...
}
.then {
	... // Never runs in case of cancel
}

Asyncable

Asyncable protocol represents an asynchronous task type that can be suspended, resumed and canceled:

public protocol Asyncable {
	func suspend() // Temporarily suspends a task.
	func resume() // Resumes the task, if it is suspended.
	func cancel() // Cancels the task.
}

Promise can manage an asynchronous task when it wraps one. For instance it's useful for network requests:

// The wrapped asynchronous task must be conformed to `Asyncable` protocol.
extension URLSessionDataTask: Asyncable {
}

let promise = Promise<Data> { resolve, reject, task in // `task` is in-out parameter
	task = URLSession.shared.dataTask(with: request) { data, response, error in
		guard error == nil else {
			reject(error!)
			return
		}
		resolve(data)
	}
	task.resume()
}

// The promise and the data task will be suspended after 2 secs and won't produce any network activity.
// but they can be resumed later.
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
	promise.suspend()
}

You can also make your custom asynchronous task that can be managed by a promise:

class TimeOutTask : Asyncable {
	let timeOut: TimeInterval
	var work: DispatchWorkItem?
	let fire: () -> Void

	init(timeOut: TimeInterval, _ fire: @escaping () -> Void) {
		self.timeOut = timeOut
		self.fire = fire
	}

	// MARK: Asyncable

	func suspend() {
		cancel()
	}

	func resume() {
		work = DispatchWorkItem(block: self.fire)
		DispatchQueue.global().asyncAfter(deadline: .now() + timeOut, execute: work!)
	}

	func cancel() {
		work?.cancel()
		work = nil
	}
}

// Promise
let promise = Promise<String> { resolve, reject, task in // `task` is in-out parameter
	task = TimeOutTask(timeOut: 3) {
		resolve("timed out") // Won't be called
	}
	task.resume()
}
.then { text in
	print(text) // Won't be called
}

// Both the promise and the timed out task will be canceled after 1 sec
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
	promise.cancel()
}

Sample

There are two variants of code to fetch avatars of first 30 GitHub users that use fetch(path:String)-> Promise<Data> utility function and User struct to parse a json response.

Using then:

fetch("https://api.github.com/users") // Load json with users
.then { data in
	try JSONDecoder().decode([User].self, from: data) // Parse json
}
.then { users -> Promise<Array<Data>> in
	guard users.count > 0 else {
		throw "Users list is empty"
	}
	return Promise.all( // Load avatars of all users
		users
		.map { $0.avatar_url }
		.map { fetch($0) }
	)
}
.then { results in
	results.map { UIImage(data: $0) } // Create array of images
}
.then(.main) { images in
	print(images.count) // Print a count of images on the main queue
}
.catch { error in
	print("Error: \(error)")
}

Using async/await:

async {
	let usersData = try fetch("https://api.github.com/users").await() // Load json with users

	let users = try JSONDecoder().decode([User].self, from: usersData) // Parse json
	guard users.count > 0 else {
		throw "Users list is empty"
	}

	let imagesData = try async.all( // Load avatars of all users
		users
			.map { $0.avatar_url }
			.map { fetch($0) }
	).await()

	let images = imagesData.map { UIImage(data: $0) } // Create array of images

	async(.main) {
		print(images.count) // Print a count of images on the main queue
	}
}
.catch { error in
	print("Error: \(error)")
}

For more samples see PromiseQTests.swift.

Installation

Swift Package Manager (SPM)

Select Xcode > File > Swift Packages > Add Package Dependency... > Paste https://github.com/ikhvorost/PromiseQ.git and then import PromiseQ in source files.

For Swift packages:

dependencies: [
    .package(url: "https://github.com/ikhvorost/PromiseQ.git", from: "1.0.0")
]

Manual

Just copy source files to your project.

License

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