Hydra

Swift 中轻量级、功能完善的 Promise、Async & Await 库

这是什么?

Hydra 是一个功能完善的轻量级库,它允许你在 Swift 3.x/4.x 中编写更好的异步代码。它部分基于 JavaScript A+ 规范,并且还实现了现代结构,例如 await (如 ES8 (ECMAScript 2017) 中的 Async/Await 规范 或 C# 中所见),它允许你以同步的方式编写异步代码。 Hydra 支持所有最吸引人的操作符,例如 always、validate、timeout、retry、all、any、pass、recover、map、zip、defer 和 retry。
开始使用 Hydra 编写更好的异步代码!

有关 Hydra 如何工作的更详细的介绍可以在 ARCHITECTURE 文件中或 Medium 上找到。

❤️ 你的支持

你好,各位开发者!
你知道,维护和开发工具需要消耗资源和时间。 虽然我喜欢制作它们,你的支持对于让我继续开发至关重要

如果你正在使用 SwiftLocation 或我的任何其他作品,请考虑以下选项

介绍

什么是 Promise?

Promise 是一种表示在未来的某个时间点存在或因错误而失败的值的方法。 你可以把它想象成 Swift 的 Optional:它可能是一个值,也可能不是。一篇更详细的文章解释了 Hydra 的实现方式可以在这里找到

每个 Promise 都是强类型的:这意味着你使用期望的值类型创建它,并且你将确保在 Promise 被解析(确切的术语是 fulfilled)时收到它。

事实上,Promise 是一个代理对象;由于系统知道成功值的样子,因此组合异步操作是一项简单的任务; 使用 Hydra 你可以

更新到 >=0.9.7

自 0.9.7 起,Hydra 实现了可取消的 Promise。 为了支持这个新特性,我们稍微修改了 PromiseBody 签名; 为了使你的源代码兼容,你只需要添加第三个参数以及 resolverejectoperationoperation 封装了支持 Invalidation Token 的逻辑。 它只是一个类型为 PromiseStatus 的对象,你可以查询该对象以查看 Promise 是否从外部标记为已取消。 如果你对在 Promise 声明中使用它不感兴趣,只需将其标记为 _

总而言之,你的代码

return Promise<Int>(in: .main, token: token, { resolve, reject in ...

需要变为

return Promise<Int>(in: .main, token: token, { resolve, reject, operation in // or resolve, reject, _

创建一个 Promise

创建一个 Promise 非常简单; 你需要指定 context(一个 GCD 队列),你的异步操作将在其中执行,并将你自己的异步代码添加为 Promise 的 body

这是一个简单的异步图像下载器

func getImage(url: String) -> Promise<UIImage> {
    return Promise<UIImage>(in: .background, { resolve, reject, _ in
        self.dataTask(with: request, completionHandler: { data, response, error in
            if let error = error {
                reject(error)
            } else if let data = data, let response = response as? HTTPURLResponse {
                resolve((data, response))
            } else {
                reject("Image cannot be decoded")
            }
        }).resume()
    })
}

你只需要记住几件事

如何使用 Promise

使用 Promise 甚至更容易。
你可以使用 then 函数获取 Promise 的结果; 当你的 Promise 以预期值 fullfill 时,它将被自动调用。 所以

getImage(url).then(.main, { image in
	myImageView.image = image
})

如你所见,即使 then 也可以指定一个上下文(默认情况下 - 如果未指定 - 是主线程):这表示将在其中执行 then 的代码块的 GCD 队列(在我们的例子中,我们需要更新一个 UI 控件,因此我们需要在 .main 线程中执行它)。

但是,如果你的 Promise 因网络错误或图像无法解码而失败怎么办? catch 函数允许你处理 Promise 的错误(使用多个 Promise,你也可以拥有一个单独的错误入口点并降低复杂性)。

getImage(url).then(.main, { image in
	myImageView.image = image
}).catch(.main, { error in
	print("Something bad occurred: \(error)")
})

链接多个 Promise

链接 Promise 是掌握 Hydra 的下一步。 假设你定义了一些 Promise

func loginUser(_ name:String, _ pwd: String)->Promise<User>
func getFollowers(user: User)->Promise<[Follower]>
func unfollow(followers: [Follower])->Promise<Int>

每个 Promise 都需要使用前一个 Promise 的 fulfilled 值; 另外,其中一个错误应该中断整个链。
使用 Hydra 这样做非常简单

loginUser(username,pass).then(getFollowers).then(unfollow).then { count in
	print("Unfollowed \(count) users")
}.catch { err in
	// Something bad occurred during these calls
}

很简单,对吧?(请注意:在此示例中未指定上下文,因此使用默认的 .main)。

可取消的 Promise

可取消的 Promise 是一项非常敏感的任务;默认情况下,Promise 是不可取消的。 Hydra 允许您通过实现 InvalidationToken 从外部取消 promise。 InvalidationToken 是一个具体的开放类,它符合 InvalidatableProtocol 协议。 它必须至少实现一个名为 isCancelledBool 属性。

isCancelled 设置为 true 时,表示 promise 外部的某人想要取消该任务。

您有责任从 Promise 的主体内部通过询问 operation.isCancelled 来检查此变量的状态。 如果 true,您可以尽最大努力取消操作; 在操作结束时,只需调用 cancel() 并停止工作流程。

您的 promise 还必须使用此 token 实例进行初始化。

这是一个带有 UITableViewCell 的具体示例:在使用表格单元格时,通常需要忽略 promise 的结果。 为此,每个单元格都可以保留一个 InvalidationTokenInvalidationToken 是一个可以失效的执行上下文。 如果上下文失效,则传递给它的块将被丢弃并且不会执行。

要将此与表格单元格一起使用,队列应在 prepareForReuse() 上失效并重置。

class SomeTableViewCell: UITableViewCell {
    var token = InvalidationToken()

	func setImage(atURL url: URL) {
		downloadImage(url).then(in: .main, { image in
			self.imageView.image = image
		})
	}

	override func prepareForReuse() {
		super.prepareForReuse()
		token.invalidate() // stop current task and ignore result
		token = InvalidationToken() // new token
	}

	func downloadImage(url: URL) -> Promise<UIImage> {
		return Promise<Something>(in: .background, token: token, { (resolve, reject, operation) in
		// ... your async operation

		// somewhere in your Promise's body, for example in download progression
		// you should check for the status of the operation.
		if operation.isCancelled {
			// operation should be cancelled
			// do your best to cancel the promise's task
			operation.cancel() // request to mark the Promise as cancelled
			return // stop the workflow! it's important
		}
		// ... your async operation
		})
	}
}

Await & Async: 以同步方式编写异步代码

您是否梦想过像编写同步代码一样编写异步代码? Hydra 受到 ES8 (ECMAScript 2017) 中的 Async/Await 规范 的启发,它提供了一种以顺序方式编写异步代码的强大方法。

使用 asyncawait 非常简单。

注意:从 Hydra 2.0.6 开始,await 函数在 Hydra.await() 函数下可用,以便抑制 Xcode 12.5+ 警告(await 很快将成为 Swift 标准函数!)

例如,上面的代码可以直接重写为

// With `async` we have just defined a Promise which will be executed in a given
// context (if omitted `background` thread is used) and return an Int value.
let asyncFunc = async({ _ -> Int in // you must specify the return of the Promise, here an Int
	// With `await` the async code is resolved in a sync manner
	let loggedUser = try Hydra.await(loginUser(username,pass))
	// one promise...
	let followersList = try Hydra.await(getFollowers(loggedUser))
	// after another...
	let countUnfollowed = try Hydra.await(unfollow(followersList))
	// ... linearly
	// Then our async promise will be resolved with the end value
	return countUnfollowed
}).then({ value in // ... and, like a promise, the value is returned
	print("Unfollowed \(value) users")
})

就像魔法一样! 您的代码将在 .background 线程中运行,并且您只有在每次调用完成时才能获得结果。 同步风格的异步代码!

重要提示await 是一个使用信号量实现的阻塞/同步函数。 因此,它不应在主线程中调用; 这就是我们使用 async 对其进行封装的原因。 在主线程中执行它也会阻塞 UI。

async 函数可以在两种不同的选项中使用

正如我们所说,我们也可以将 async 与您自己的块一起使用(不使用 promise); async 接受上下文(一个 GCD 队列)和可选的启动延迟间隔。 下面是一个 async 函数的示例,它将在后台无延迟地执行

async({
	print("And now some intensive task...")
	let result = try! Hydra.await(.background, { resolve,reject, _ in
		delay(10, context: .background, closure: { // jut a trick for our example
			resolve(5)
		})
	})
	print("The result is \(result)")
})

还有一个 await 运算符

示例

async({
	// AWAIT OPERATOR WITH DO/CATCH: `..`
	do {
		let result_1 = try ..asyncOperation1()
		let result_2 = try ..asyncOperation2(result_1) // result_1 is always valid
	} catch {
		// something goes bad with one of these async operations
	}
})

// AWAIT OPERATOR WITH NIL-RESULT: `..!`
async({
	let result_1 = ..!asyncOperation1() // may return nil if promise fail. does not throw!
	let result_2 = ..!asyncOperation2(result_1) // you must handle nil case manually
})

当您使用这些方法并且正在进行异步操作时,请注意不要在主线程中执行任何操作,否则您有陷入死锁的风险。

最后一个示例展示了如何使用可取消的 async

func test_invalidationTokenWithAsyncOperator() {

// create an invalidation token
let invalidator: InvalidationToken = InvalidationToken()

async(token: invalidator, { status -> String in
	Thread.sleep(forTimeInterval: 2.0)
	if status.isCancelled {
		print("Promise cancelled")
	} else {
		print("Promise resolved")
	}
	return "" // read result
}).then { _ in
	// read result
}

// Anytime you can send a cancel message to invalidate the promise
invalidator.invalidate()
}

Await 一个 zip 运算符来解析所有 promise

Await 也可以与 zip 结合使用,以解析列表中的所有 promise

let (resultA,resultB) = Hydra.await(zip(promiseA,promiseB))
print(resultA)
print(resultB)

所有功能

由于 promise 形式化了成功和失败块的外观,因此可以在它们之上构建行为。 Hydra 支持

always

如果您想在 promise 完成时执行代码,而不管它是否成功或失败,always 函数非常有用。

showLoadingHUD("Logging in...")
loginUser(username,pass).then { user in
	print("Welcome \(user.username)")
}.catch { err in
 	print("Cannot login \(err)")
}.always {
 	hideLoadingHUD()
}

validate

validate 是一个采用谓词的函数,如果该谓词失败,则拒绝 promise 链。

getAllUsersResponse().validate { httpResponse in
	guard let httpResponse.statusCode == 200 else {
		return false
	}
	return true
}.then { usersList in
	// do something
}.catch { error in
	// request failed, or the status code was != 200
}

timeout

timeout 允许您将超时计时器附加到 Promise; 如果它在经过的时间间隔之前未解析,它将被 .timeoutError 拒绝。

loginUser(username,pass).timeout(.main, 10, .MyCustomTimeoutError).then { user in
	// logged in
}.catch { err in
	// an error has occurred, may be `MyCustomTimeoutError
}

all

all 是一种静态方法,它等待您给它的所有 promise 完成,一旦它们完成,它就会用所有已完成值的数组(按顺序)完成自身。

如果一个 Promise 失败,链也会失败,并显示相同的错误。

所有 Promise 的执行都是并行完成的。

let promises = usernameList.map { return getAvatar(username: $0) }
all(promises).then { usersAvatars in
	// you will get an array of UIImage with the avatars of input
	// usernames, all in the same order of the input.
	// Download of the avatar is done in parallel in background!
}.catch { err in
	// something bad has occurred
}

如果您想为 all 操作符添加 Promise 执行并发限制,以避免大量资源占用,可以使用 concurrency 选项。

let promises = usernameList.map { return getAvatar(username: $0) }
all(promises, concurrency: 4).then { usersAvatars in
	// results of usersAvatars is same as `all` without concurrency.
}.catch { err in
	// something bad has occurred
}

any

any 可以轻松处理竞争条件:只要输入列表中的一个 Promise 解决 (resolve),处理程序就会被调用,并且不会再次被调用。

let mirror_1 = "https://mirror1.mycompany.com/file"
let mirror_2 = "https://mirror2.mycompany.com/file"

any(getFile(mirror_1), getFile(mirror_2)).then { data in
	// the first fulfilled promise also resolve the any Promise
	// handler is called exactly one time!
}

pass

pass 对于在 Promise 链的中间执行操作而不更改 Promise 的类型非常有用。您也可以拒绝整个链。您还可以从 tap 处理程序返回一个 Promise,并且链将等待该 Promise 解决(请参见下面的示例中的第二个 then)。

loginUser(user,pass).pass { userObj in 
	print("Fullname is \(user.fullname)")
}.then { userObj in
	updateLastActivity(userObj)
}.then { userObj in
	print("Login succeded!")
}

recover

recover 允许您通过返回另一个 Promise 来恢复失败的 Promise。

let promise = Promise<Int>(in: .background, { fulfill, reject in
	reject(AnError)
}).recover({ error in
    return Promise(in: .background, { (fulfill, reject) in
		fulfill(value)
    })
})

map

Map 用于将项目列表转换为 promise,并并行或串行地解决它们。

[urlString1,urlString2,urlString3].map {
	return self.asyncFunc2(value: $0)
}.then(.main, { dataArray in
	// get the list of all downloaded data from urls
}).catch({
	// something bad has occurred
})

zip

zip 允许您连接不同的 promise (2, 3 或 4) 并返回一个包含它们结果的元组。 Promise 是并行解决的。

zip(a: getUserProfile(user), b: getUserAvatar(user), c: getUserFriends(user))
  .then { profile, avatar, friends in
	// ... let's do something
}.catch {
	// something bad as occurred. at least one of given promises failed
}

defer

顾名思义,defer 会将 Promise 链的执行从当前时间延迟若干秒。

asyncFunc1().defer(.main, 5).then...

retry

如果源链接的 promise 以拒绝 (rejection) 结束,则 retry 操作符允许您执行源链接的 promise。如果达到尝试次数,promise 仍然被拒绝,链接的 promise 也会被拒绝,并伴随相同的源错误。
Retry 还支持 delay 参数,该参数指定在新的尝试之前要等待的秒数 (2.0.4+)。

// try to execute myAsyncFunc(); if it fails the operator try two other times
// If there is not luck for you the promise itself fails with the last catched error.
myAsyncFunc(param).retry(3).then { value in
	print("Value \(value) got at attempt #\(currentAttempt)")
}.catch { err in
	print("Failed to get a value after \(currentAttempt) attempts with error: \(err)")
}

条件重试允许您控制如果重试以拒绝结束是否可重试。

// If myAsyncFunc() fails the operator execute the condition block to check retryable.
// If return false in condition block, promise state rejected with last catched error.
myAsyncFunc(param).retry(3) { (remainAttempts, error) -> Bool in
  return error.isRetryable
}.then { value in
	print("Value \(value) got at attempt #\(currentAttempt)")
}.catch { err in
	print("Failed to get a value after \(currentAttempt) attempts with error: \(err)")
}

cancel

当通过调用 operation.cancel() 函数从 Promise 的主体将 promise 标记为 cancelled 时,将调用 cancel。 有关更多信息,请参见 可取消的 Promises

asyncFunc1().cancel(.main, {
	// promise is cancelled, do something
}).then...

链接具有不同 Value 类型的 Promises

有时您可能需要链接(使用可用的操作符之一,例如 allany)返回不同类型值的 promise。 由于 Promise 的性质,您无法创建具有不同结果类型的 promise 数组。 但是,借助 void 属性,您可以将 promise 实例转换为通用的 void 结果类型。 因此,例如,您可以执行以下 Promises 并直接从 Promise 的 result 属性返回最终值。

let op_1: Promise<User> = asyncGetCurrentUserProfile()
let op_2: Promise<UIImage> = asyncGetCurrentUserAvatar()
let op_3: Promise<[User]> = asyncGetCUrrentUserFriends()

all(op_1.void,op_2.void,op_3.void).then { _ in
	let userProfile = op_1.result
	let avatar = op_2.result
	let friends = op_3.result
}.catch { err in
	// do something
}

安装

您可以使用 CocoaPods、Carthage 和 Swift 包管理器安装 Hydra

CocoaPods

use_frameworks!
pod 'HydraAsync'

Carthage

github 'malcommac/Hydra'

Swift Package Manager

在您的 Package.swift 中将 Hydra 添加为依赖项

  import PackageDescription

  let package = Package(name: "YourPackage",
    dependencies: [
      .Package(url: "https://github.com/malcommac/Hydra.git", majorVersion: 0),
    ]
  )

考虑 ❤️ 支持此库的开发!

要求

当前版本兼容:

贡献

版权 & 鸣谢

SwiftLocation 目前由 Daniele Margutti 拥有和维护。
您可以在 Twitter 上关注我:@danielemargutti
我的网站是:https://www.danielemargutti.com

本软件基于 MIT 许可证授权。

关注我: