0%

PromiseKit

then and done

典型使用:

1
2
3
4
5
6
7
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}

如果这段代码使用了completion handlers,它将如下所示:

1
2
3
4
5
6
7
8
9
login { creds, error in
if let creds = creds {
fetch(avatar: creds.user) { image, error in
if let image = image {
self.imageView = image
}
}
}
}

then 只是构造completion handlers的另一种方式, 但也远不止如此。在我们理解的最初阶段,它主要有助于可读性。上面的promise链很容易浏览和理解:一个异步操作一行一行地通向另一个。鉴于Swift的当前状态,它尽可能接近程序代码。

donethen ,但是你不能返回一个promise. 这通常是链条中success部分的终点. 上面, 您可以看到我们在 done 时收到了最终的图像,并使用它来设置用户界面。

两种实现方法的比较:

1
2
3
4
5
6
func login() -> Promise<Creds>

// Compared with:

func login(completion: (Creds?, Error?) -> Void)
// ^^ ugh. Optionals. Double optionals.

区别在于,对于Promise,您的函数返回Promise,而不是接受和运行回调。链中的每个handler都会返回一个PromisePromise对象定义了then方法,该方法在链的执行之前等待Promise的完成。锁链按程序解决,一次一个Promise

Promise 代表异步任务的返回值。它有一个表示它包装的对象类型的类型。例如,在上面的例子中,login是一个返回Promise的函数,该Promise将代表Creds实例。

Note: done is new to PromiseKit 5. We previously defined a variant of then that
did not require you to return a promise. Unfortunately, this convention often confused
Swift and led to odd and hard-to-debug error messages. It also made using PromiseKit
more painful. The introduction of done lets you type out promise chains that
compile without additional qualification to help the compiler figure out type information.


您可能会注意到,与completion模式不同,promise链似乎
忽略errors。事实并非如此!事实上,它有相反的效果:promise
链使得errors处理更容易访问,也使得errors更难忽略。

catch

有了promise,errors就会沿着promise链级联,确保你的应用程序
健壮,代码清晰:

1
2
3
4
5
6
7
8
9
firstly {
login()
}.then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}.catch {
// any errors in the whole chain land here
}

Swift emits a warning if you forget to catch a chain. But we’ll
talk about that in more detail later.

每个promise都是一个对象,代表一个单独的异步任务。
如果任务失败, promise就会变成rejected。 Chains that contain rejected
包含rejected的promise链跳过所有后续的promise。相反,执行下一个捕获。
(严格来说,所有后续的捕获处理程序都会被执行。)

completion handlerpromise的比较

1
2
3
4
5
6
7
8
9
10
11
func handle(error: Error) {
//…
}

login { creds, error in
guard let creds = creds else { return handle(error: error!) }
fetch(avatar: creds.user) { image, error in
guard let image = image else { return handle(error: error!) }
self.imageView.image = image
}
}

guarderror handler的使用有所帮助,但是promise链
可读性不言自明。

ensure

我们已经学会了合成异步性。接下来,让我们扩展:

1
2
3
4
5
6
7
8
9
10
11
12
firstly {
UIApplication.shared.isNetworkActivityIndicatorVisible = true
return login()
}.then {
fetch(avatar: $0.user)
}.done {
self.imageView = $0
}.ensure {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch {
//…
}

不管你的链条的结果是什么——失败还是成功 ensure
handler 总是被调用.

ensure模式与其等价的完成处理程序进行比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UIApplication.shared.isNetworkActivityIndicatorVisible = true

func handle(error: Error) {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
//…
}

login { creds, error in
guard let creds = creds else { return handle(error: error!) }
fetch(avatar: creds.user) { image, error in
guard let image = image else { return handle(error: error!) }
self.imageView.image = image
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}
}

对某人来说,修改这个代码并忘记取消设置是非常容易的
导致错误的活动指示器。有了promises,这种类型的错误是
几乎不可能:Swift编译器拒绝你在没有
使用promises。您几乎不需要审查拉动式请求。

Note: PromiseKit has perhaps capriciously switched between the names always
and ensure for this function several times in the past. Sorry about this. We suck.

You can also use finally as an ensure that terminates the promise chain and does not return a value:
您也可以使用finally作为结束的promise链且不返回值的ensure:

1
2
3
4
5
6
7
8
9
10
11
spinner(visible: true)

firstly {
foo()
}.done {
//…
}.catch {
//…
}.finally {
self.spinner(visible: false)
}

when

对多个异步操作的completion handlers反应是缓慢的,意味着连续执行:

1
2
3
4
5
operation1 { result1 in
operation2 { result2 in
finish(result1, result2)
}
}

快速(parallel)路径代码使代码不太清晰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var result1: …!
var result2: …!
let group = DispatchGroup()
group.enter()
group.enter()
operation1 {
result1 = $0
group.leave()
}
operation2 {
result2 = $0
group.leave()
}
group.notify(queue: .main) {
finish(result1, result2)
}

如果使用Promises就更清晰了:

1
2
3
4
5
firstly {
when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
//…
}

when promise代表:等待它们解决并返回包含结果的promise。

与任何promise链一样,如果任何组件promise失败,该链将调用下一个catch

PromiseKit Extensions

当我们使用PromiseKit, 我们是想想通过promise来实现
异步行为。因此,只要有可能,我们会对苹果的APIs进行扩展,重新构建
promise方面的应用编程接口。例如:

1
2
3
4
5
6
7
firstly {
CLLocationManager.promise()
}.then { location in
CLGeocoder.reverseGeocode(location)
}.done { placemarks in
self.placemark.text = "\(placemarks.first)"
}

要使用这些扩展,您需要指定子代码:

1
2
3
pod "PromiseKit"
pod "PromiseKit/CoreLocation"
pod "PromiseKit/MapKit"

All of these extensions are available at the PromiseKit organization.
Go there to see what’s available and to read the source code and documentation. Every file and function
has been copiously documented.

We also provide extensions for common libraries such as Alamofire.

Making Promises

The standard extensions will take you a long way, but sometimes you’ll still need to start chains
of your own. Maybe you’re using a third party API that doesn’t provide promises, or perhaps you wrote
your own asynchronous system. Either way, it’s easy to add promises. If you look at the code of the
standard extensions, you’ll see that it uses the same approach described below.

标准的扩展会让你走很长一段路,但是有时候你仍然需要启动链
你自己的。也许你使用的是不提供promises的第三方应用编程接口,或者你写道
你自己的异步系统。不管怎样,添加promises都很容易。如果你看看
标准扩展,您会看到它使用了下面描述的相同方法。

Let’s say we have the following method:

1
func fetch(completion: (String?, Error?) -> Void)

How do we convert this to a promise? Well, it’s easy:

1
2
3
func fetch() -> Promise<String> {
return Promise { fetch(completion: $0.resolve) }
}

You may find the expanded version more readable:

1
2
3
4
5
6
7
func fetch() -> Promise<String> {
return Promise { seal in
fetch { result, error in
seal.resolve(result, error)
}
}
}

The seal object that the Promise initializer provides to you defines
many methods for handling garden-variety completion handlers. It even
covers a variety of rarer situations, thus making it easy for you to add
promises to an existing codebase.

Note: We tried to make it so that you could just do Promise(fetch), but we
were not able to make this simpler pattern work universally without requiring
extra disambiguation for the Swift compiler. Sorry; we tried.

Note: In PMK 4, this initializer provided two parameters to your closure:
fulfill and reject. PMK 5 and 6 give you an object that has both fulfill and
reject methods, but also many variants of the method resolve. You can
typically just pass completion handler parameters to resolve and let Swift figure
out which variant to apply to your particular case (as shown in the example above).

Note Guarantees (below) have a slightly different initializer (since they
cannot error) so the parameter to the initializer closure is just a closure. Not
a Resolver object. Thus do seal(value) rather than seal.fulfill(value). This
is because there is no variations in what guarantees can be sealed with, they can
only fulfill.

Guarantee<T>

Since PromiseKit 5, we have provided Guarantee as a supplementary class to
Promise. We do this to complement Swift’s strong error handling system.

Guarantees never fail, so they cannot be rejected. A good example is after:

1
2
3
4
5
firstly {
after(seconds: 0.1)
}.done {
// there is no way to add a `catch` because after cannot fail.
}

Swift warns you if you don’t terminate a regular Promise chain (i.e., not
a Guarantee chain). You’re expected to silence this warning by supplying
either a catch or a return. (In the latter case, you will then have to catch
at the point where you receive that promise.)

Use Guarantees wherever possible so that your code has error handling where
it’s required and no error handling where it’s not required.

In general, you should be able to use Guarantees and Promises interchangeably,
We have gone to great lengths to try and ensure this, so please open a ticket
if you find an issue.


If you are creating your own guarantees the syntax is simpler than that of promises;

1
2
3
4
5
6
7
func fetch() -> Promise<String> {
return Guarantee { seal in
fetch { result in
seal(result)
}
}
}

Which could be reduced to:

1
2
3
func fetch() -> Promise<String> {
return Guarantee(resolver: fetch)
}

map, compactMap, etc.

then provides you with the result of the previous promise and requires you to return
another promise.

map provides you with the result of the previous promise and requires you to return
an object or value type.

compactMap provides you with the result of the previous promise and requires you
to return an Optional. If you return nil, the chain fails with
PMKError.compactMap.

Rationale: Before PromiseKit 4, then handled all these cases, and it was
painful. We hoped the pain would disappear with new Swift versions. However,
it has become clear that the various pain points are here to stay. In fact, we
as library authors are expected to disambiguate at the naming level of our API.
Therefore, we have split the three main kinds of then into then, map and
done. After using these new functions, we realized this is much nicer in practice,
so we added compactMap as well (modeled on Optional.compactMap).

compactMap facilitates quick composition of promise chains. For example:

1
2
3
4
5
6
7
8
9
10
firstly {
URLSession.shared.dataTask(.promise, with: rq)
}.compactMap {
try JSONSerialization.jsonObject($0.data) as? [String]
}.done { arrayOfStrings in
//…
}.catch { error in
// Foundation.JSONError if JSON was badly formed
// PMKError.compactMap if JSON was of different type
}

Tip: We also provide most of the functional methods you would expect for sequences,
e.g., map, thenMap, compactMapValues, firstValue, etc.

get

We provide get as a done that returns the value fed to get.

1
2
3
4
5
6
7
firstly {
foo()
}.get { foo in
//…
}.done { foo in
// same foo!
}

tap

We provide tap for debugging. It’s the same as get but provides the
Result<T> of the Promise so you can inspect the value of the chain at this
point without causing any side effects:

1
2
3
4
5
6
7
8
9
firstly {
foo()
}.tap {
print($0)
}.done {
//…
}.catch {
//…
}

Supplement

firstly

We’ve used firstly several times on this page, but what is it, really? In fact,
it is just syntactic sugar.
You don’t really need it, but it