Swiftpack.co - Package - ikhvorost/PromiseQ

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 array 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 array 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

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()

Sample

There are to variants of code to fetch avatars of first 30 GitHub users that use fetch(path:String) 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)")
}

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.

Github

link
Stars: 1

Dependencies

Used By

Total: 0