PromiseQ

Language: Swift Platform: iOS 8+/macOS10.11 SPM compatible build & test codecov swift doc coverage

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

Swift 快速、强大且轻量级的 Promise 实现。

特性

高性能

如果 Promise 闭包在同一个队列上,则会同步地一个接一个地调用,否则会异步调用。

轻量级

整个实现由数百行代码组成。

内存安全

PromiseQ 基于 struct 和回调堆栈,可以避免许多内存管理问题,例如循环引用等。

标准 API

基于 JavaScript Promises/A+ 规范,支持 async/await,并且还包括标准方法:Promise.allPromise.racePromise.anyPromise.resolve/reject

暂停

这是一个额外的有用功能,可以暂停 Promise 的执行,并在以后恢复它们。 暂停不会影响已经开始执行的 Promise,它会停止下一个 Promise 的执行。

取消

可以取消所有排队的 Promise,以停止异步逻辑。 取消不会影响已经开始执行的 Promise,它会取消 Promise 并停止链中下一个 Promise 的执行。

基本用法

Promise

Promise 是一种泛型类型,表示异步操作,您可以使用闭包以简单的方式创建它,例如:

Promise {
    try String(contentsOfFile: file)
}

提供的闭包在创建 Promise 后异步调用。 默认情况下,闭包在全局默认队列 DispatchQueue.global() 上运行,但您也可以指定需要运行的队列。

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

当闭包返回一个值时,Promise 可以被解析 (resolve);当闭包抛出一个错误时,Promise 可以被拒绝 (reject)。

此外,闭包可以使用 resolve/reject 回调来解决 Promise 以进行异步任务。

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

then

它接受一个提供的闭包并返回一个新的 Promise。 闭包在当前 Promise 被解析时运行,并接收结果。

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

通过这种方式,我们可以通过 Promise 链传递结果。

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

此外,闭包可以返回一个 Promise,它将被注入到 Promise 链中。

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

catch

它接受一个闭包并返回一个新的 Promise。 闭包在 Promise 被拒绝时运行,并接收错误。

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

无论 Promise 是被解析还是被拒绝,这都会始终运行,因此它是执行清理等操作的良好处理程序。

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

这些用于兼容性,例如,当只需要返回一个已解析或已拒绝的 Promise 时。

Promise.resolve 创建一个具有给定值的已解析 Promise。

Promise {
    return 200
}

// Same as above
Promise.resolve(200)

Promise.reject 创建一个具有给定错误的已拒绝 Promise。

Promise {
    throw error
}

// Same as above
Promise<Void>.reject(error)

Promise.all

它返回一个 Promise,该 Promise 在提供的列表中所有列出的 Promise 都被解析时解析,并且其结果数组将成为其结果。 如果任何 Promise 被拒绝,则 Promise.all 返回的 Promise 会立即被拒绝,并出现该错误。

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"]

您可以设置 settled=true 参数以创建一个 Promise,该 Promise 在所有列出的 Promise 都被解决时解析,无论其结果如何。

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 或 Promise 数组为空,则 Promise 使用空数组解析。

Promise.race

它创建一个 Promise,该 Promise 仅等待给定列表中第一个已解决的 Promise,并获取其结果或错误。

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"

如果没有 Promise 或 Promise 数组为空,则 Promise 会被 PromiseError.empty 错误拒绝。

Promise.any

它与 Promise.race 类似,但仅等待第一个已完成的 Promise,并获取其结果。

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

如果没有 Promise 或 Promise 数组为空,则 Promise 会被 PromiseError.empty 错误拒绝。

如果所有给定的 Promise 都被拒绝,则返回的 Promise 将被 PromiseError.aggregate 拒绝 - 这是一种特殊的错误,用于存储所有 Promise 错误。

Promise.any(
    Promise { resolve, reject in
        reject("Error")
    },
    Promise { resolve, reject in
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // Waits 1 sec
            reject("Fail")
        }
    }
)
.catch { error in
    if case let PromiseError.aggregate(errors) = error {
        print(errors, "-", error.localizedDescription)
    }
}
// Prints: '["Error", "Fail"]' - All Promises rejected.

高级用法

timeout

timeout 参数允许等待 Promise 一段时间间隔,如果 Promise 没有在给定时间内解析,则使用 PromiseError.timedOut 错误拒绝它。

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.localizedDescription)
    }
    else {
        handleError(error)
    }
}

retry

retry 参数提供了在 Promise 被拒绝时重新尝试任务的能力。 默认情况下,只有一次尝试解析 Promise,但您可以使用此参数增加尝试次数。

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

async/await

这是一种特殊的表示法,可以更舒适地使用 Promise,并且易于理解和使用。

asyncPromise 的别名,因此您也可以使用它来创建 Promise。

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

await() 是一个函数,同步地等待 Promise 的结果,否则会抛出错误。

let text = try readFile("file.txt").await()
// OR
let text = try `await` { readFile("file.txt") }

为了避免阻塞当前队列(例如主 UI 队列),我们可以将 await() 传递到另一个 Promise(async 块)中,并像往常一样使用 catch 来处理错误。

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

suspend/resume

suspend() 临时暂停一个 Promise。 暂停不会影响已经开始执行的当前 Promise,它会停止链中下一个 Promise 的执行。 Promise 可以在以后使用 resume() 继续执行。

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

cancel

取消 Promise 的执行并使用 PromiseError.cancelled 错误拒绝它。 取消不会影响已经开始执行的 Promise,它会拒绝 Promise 并停止链中下一个 Promise 的执行。

let promise = Promise {
    return "Text" // Never runs
}
.then { text in
    print(text) // Never runs
}
.catch { error in
    if case PromiseError.cancelled = error {
        print(error.localizedDescription)
    }
}

promise.cancel()

// Prints: The Promise cancelled.

取消 Promise 不会主动停止长时间运行的处理代码的执行。 您可以使用 isCancelled 方法检查 Promise 是否已取消,如果该方法返回 true,则停止您的代码。

let promise = Promise { 10_000 }

promise.then { count in
    for i in 0..<count {
        guard !promise.isCancelled else {
            return
        }
        ...
    }
}

您也可以在某些条件下中断 Promise 链,以便在 Promise 链的任何闭包中调用 cancel,例如:

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 协议表示可以暂停、恢复和取消的异步任务类型。

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

Promise 可以在包装异步任务时管理它。 例如,它对于网络请求很有用。

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

您也可以创建自定义的异步任务,该任务可以由 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()
}

网络请求

fetch

您可以使用异步 fetch 实用程序函数向服务器发送网络请求并在需要时加载新信息。 它立即启动请求并返回一个 Promise,调用代码应该使用该 Promise 来获取结果。 fetch 返回的 Promise 在服务器响应后立即使用 HTTPResponse 对象解析,您可以使用它来访问响应属性。

例如

fetch("https://api.github.com/users/technoweenie") // Get github user's info
.then { response in
    guard response.ok else { // Check for the HTTP status code
        throw response.statusCodeDescription
    }

    guard let json = response.json as? [String: Any] else { // Get json from the returned data
        throw "No JSON"
    }

    if let name = json["name"] as? String { // Get name of the user
        print(name)
    }
}
.catch {error in
    print(error.localizedDescription)
}

// Prints: risk danger olson

默认情况下,fetch 使用默认标头和空正文数据执行 GET 请求,但您可以使用可选参数更改它。

async {
    let response = try fetch(url,
        method: .POST,
        headers: ["Accept-Encoding" : "br, gzip, deflate"],
        body: data
    ).await()

    ...
}
.catch {error in
    print(error.localizedDescription)
}

默认情况下,fetch 使用 URLSession.default 发出请求,但您也可以在会话实例上调用它,并调整会话行为的各个方面,包括缓存策略、超时间隔等。

let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 120
config.httpAdditionalHeaders = ["Accept-Encoding" : "br, gzip, deflate"]
let session = URLSession(configuration: config)

async {
    let response = try session.fetch(url).await()
    ...
}
.catch {error in
    print(error.localizedDescription)
}

download

download 的工作方式类似于 fetch,但会将数据保存到文件并告知下载进度,例如:

async {
    let response = try download("http://speedtest.tele2.net/1MB.zip") { task, written, total in
        let percent = Double(written) / Double(total)
        print(percent)
    }.await()

    guard response.ok else {
        throw response.statusCodeDescription
    }

    guard let location = response.location else {
        throw "No location"
    }

    print(location)
}
.catch { error in
    print(error.localizedDescription)
}

// Prints
0.038433074951171875
0.10195541381835938
...
0.9263648986816406
1.0
file:///var/folders/nt/mrsc3jhd13j8zhrhxy4x23y40000gp/T/pq_CFNetworkDownload_t94Pig.tmp

upload

upload 的工作方式类似于 fetch,但可以将数据或文件上传到服务器并告知上传进度,例如:

async {
    let response = try upload(url, data: data) { task, sent, total in
        let percent = Double(sent) / Double(total)
        print(percent)
    }.await()

    guard response.ok else {
        throw response.statusCodeDescription
    }

    print("Uploaded")
}
.catch { error in
    print(error.localizedDescription)
}

// Prints
0.03125
0.0625
...
0.96875
1.0
Uploaded

示例

有两种代码变体来 fetch 前 30 个 GitHub 用户的头像。

使用 then

struct User : Codable {
    let login: String
    let avatar_url: String
}

fetch("https://api.github.com/users") // Load json with users
.then { response -> [User] in
    guard response.ok else {
        throw response.statusCodeDescription
    }

    guard let data = response.data else {
        throw "No data"
    }

    return try JSONDecoder().decode([User].self, from: data) // Parse json
}
.then { users -> Promise<Array<HTTPResponse>> in
    return Promise.all(
        users
        .map { $0.avatar_url }
        .map { fetch($0) }
    )
}
.then { responses in
    responses
        .compactMap { $0.data }
        .compactMap { 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.localizedDescription)
}

使用 async/await

async {
    let response = try `await` { fetch("https://api.github.com/users") }

    guard response.ok else {
        throw response.statusCodeDescription
    }

    guard let data = response.data else {
        throw "No data"
    }

    let users = try JSONDecoder().decode([User].self, from: data)

    let images = try `await` {
        Promise.all(
            users.map { fetch($0.avatar_url) }
        )
    }
    .compactMap { $0.data }
    .compactMap { UIImage(data: $0) }

    // Switch to the main queue
    async(.main) {
        print(images.count)
    }
}
.catch { error in
    print(error.localizedDescription)
}

有关更多示例,请参见 PromiseQTests.swift

安装

XCode 项目

  1. 选择 Xcode > File > Swift Packages > Add Package Dependency...
  2. 添加程序包存储库: https://github.com/ikhvorost/PromiseQ.git
  3. 在您的源文件中导入程序包: import PromiseQ

Swift Package

PromiseQ 程序包依赖项添加到您的 Package.swift 文件

let package = Package(
    ...
    dependencies: [
        .package(url: "https://github.com/ikhvorost/PromiseQ.git", from: "1.0.0")
    ],
    targets: [
        .target(name: "YourPackage",
            dependencies: [
                .product(name: "PromiseQ", package: "PromiseQ")
            ]
        ),
        ...
    ...
)

许可

PromiseQ 在 MIT 许可下可用。 有关更多信息,请参见 LICENSE 文件。