PinkyPromise
一个小型的 Promises 库。
概述
PinkyPromise 是 Swift 中实现 Promises 的一个实现。它包含两种类型
Result
- 一个值或错误。我们使用 Swift 内置的 Result 类型Promise
- 一个在调用后会在某个时间点产生一个 Result 的操作。Promises 可以组合和排序。
使用 PinkyPromise,您可以用安全、干净、Swift 式的代码运行复杂的异步操作组合。
安装
- 使用 Carthage:
github "willowtreeapps/PinkyPromise"
- 使用 Cocoapods:
pod 'PinkyPromise'
- 手动: 将
Sources
文件夹中的文件复制到您的项目中。
我应该使用这个吗?
PinkyPromise
- 轻量级
- 经过测试
- 支持 Swift 语言,具有严密的类型系统合约以及
throw
/catch
- 支持函数式风格,使用不可变值和值转换
- Objective-C 程序员学习 Swift 函数式风格的好方法
- 可以扩展您自己的 Promise 转换
PinkyPromise 旨在成为一款轻量级工具,但它能完成很多繁重的工作。更复杂的实现包括 Result 和 PromiseKit。
学习
从下面的示例部分开始。
我们还编写了一个游乐场来展示 PinkyPromise 的优势和用法。请克隆仓库并打开 Xcode 中的 PinkyPromise.playground
。
在 Results 和 Promises 之上的自然下一步是 Observable 类型。您可能可以将 PinkyPromise 作为学习我们推荐的 RxSwift 的第一步。
示例
Promise 最佳用于运行可能成功或失败的非同步操作。
Result 最佳用于表示这种操作的结果。
为什么是 Result?
iOS 上的常规非同步操作模式是函数,该函数接受参数和一个完成块,然后开始工作。完成块将在工作完成后接收可选值和可选错误。
func getString(withArgument argument: String, completion: ((String?, ErrorType?) -> Void)?) {
…
if successful {
completion?(value, nil)
} else {
completion?(nil, error)
}
}
getString(withArgument: "foo") { value, error in
if let value = value {
print(value)
} else {
print(error)
}
}
这不是由编译器保证的松散合约。我们只假设当 value
是 nil 时,error
不为 nil。
与标准的 Swift 同步方法模式进行比较:例如,Data(contentsOf:options:)
函数要么返回一个值,要么抛出一个错误,要么两者都不是,要么两者都是可选的。这是一个严密的合约。但是您不能在异步调用中使用这种模式,因为您只能从您所在的函数向后抛出,不能向前抛入完成块。
以下是您如何使用 Result 编写具有更严密的合约的非同步操作的示例。Result 是成功或失败。它可以创建为 return
或 throw
,并使用 value
检查,它将返回或抛出。
func getStringResult(withArgument argument: String, completion: ((Result<String, Error>) -> Void)?) {
…
completion?(Result {
if successful {
return value
} else {
throw error
}
})
}
getStringResult(withArgument: "foo") { result in
do {
print(try result.get())
} catch {
print(error)
}
}
在内部,Result<T, Error>
是一个具有两个情况的自定义枚举:`Success(T)` 和 `Failure(Error)`。可以使用枚举情况创建一个 Result,并使用 `switch` 检查它。但由于 Result 代表一个返回的值或一个抛出的错误,我们更喜欢以上所示的使用方式。
为什么使用Promise?
Promises可以将多个异步操作合并成一个。为了做到这一点,我们需要能够在不立即启动的情况下创建一个异步操作。
要创建一个新的Promise,你需要用任务来创建它。任务是一个自身包含完成块的块,通常称为fulfill
。Promise运行任务来完成其工作,当任务完成时,任务将Result
传递给fulfill
。(提示:用来创建这个Promise的任务与getStringResult(withArgument:)
的主体相同。)
func getStringPromise(withArgument argument: String) -> Promise<String> {
return Promise { fulfill in
…
fulfill(Result {
if successful {
return value
} else {
throw error
}
})
}
}
let stringPromise = getStringPromise(withArgument: "bar")
stringPromise
捕获了它的任务,而任务捕获了它的参数。这是一个等待开始的操作。因此,使用Promise,你可以创建操作并在以后再开始它们。你可以多次开始它们,也可以一次也不开始。
接下来,我们通过向call
方法传递完成块来请求stringPromise
运行。call
运行任务并将结果路由回完成块。当Promise完成时,我们的完成块将收到结果,并且可以使用try
和catch
获取值或错误。
stringPromise.call { result in
do {
print(try result.get())
} catch {
print(error)
}
}
正如我们所看到的,在Promise中,提供参数和提供完成块是两个不同的事件。Promise最大的优势是,在这两个事件之间,要做的任务作为不可变值存在。在函数式样式中,不可变值可以被转换和组合。
这是一个由多个Promise组成的复杂Promise的示例
let getFirstThreeChildrenOfObjectWithIDPromise =
getStringPromise(withArgument: "baz") // Promise<String>
.flatMap { objectID in
// String -> Promise<ModelObject>
Queries.getObjectPromise(withID: objectID)
}
.map { object in
// ModelObject -> [String]
let childObjectIDs = object.childObjectIDs
let count = max(3, childObjectIDs.count)
return childObjectIDs[0..<count]
}
.flatMap { childObjectIDs in
// [String] -> Promise<[ModelObject]>
zipArray(childObjectIDs.map { childObjectID
// String -> Promise<ModelObject>
Queries.getObjectPromise(withID: childObjectID)
})
}
getFirstThreeChildrenOfObjectWithIDPromise
是一个由许多小操作组成的单个异步操作。
- 尝试为一个对象ID获取字符串。
- 如果成功,对具有该ID的对象执行API请求。
- 如果成功,从对象中收集最多三个子ID。
- 如果成功,对每个子对象同时发出请求,产生一个数组。
- 生成最多三个子对象的一个列表,或从过程中的任何步骤返回一个错误。
尽管这个操作有多个依赖于先前操作成功的步骤,但我们不需要通过编写多个完成块来协调它们。相反,我们只需要处理最终结果,利用Result提供的紧密合同。
getFirstThreeChildrenOfObjectWithIDPromise.call { [weak self] result in
do {
self?.updateViews(withObjects: try result.get())
} catch {
self?.showError(error)
}
}
测试
我们打算将PinkyPromise完全进行单元测试。
您可以在Xcode中运行测试,或使用bundle exec fastlane run_tests
和Fastlane。
我们在CircleCI上运行持续集成。
路线图
- 更多Promise转换?
为Pinky Promise做出贡献
欢迎贡献。请参阅贡献指南。