PromiseKit - CommonPatterns GitHub
PromiseKit - CommonPatterns GitHub
PromiseKit - CommonPatterns GitHub
mxcl/PromiseKit
Code Issues 35 Pull requests 7 Actions Security Insights
Dismiss
Join GitHub today
GitHub is home to over 50 million developers
working together to host and review code,
manage projects, and build software together.
Sign up
master
PromiseKit / Documentation / CommonPatterns.md
bvirlet Minor correction to chaining sequence example
10 contributors
Raw Blame
494 lines (375 sloc) 12.5 KB
Common Patterns
One feature of promises that makes them particularly useful is that they are
composable. This fact enables complex, yet safe asynchronous patterns that would
otherwise be quite intimidating when implemented with traditional methods.
Chaining
The most common pattern is chaining:
firstly {
fetch()
}.then {
map($0)
https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md 1/11
22/08/2020 PromiseKit/CommonPatterns.md at master · mxcl/PromiseKit · GitHub
}.then {
set($0)
return animate()
}.ensure {
// something that should happen whatever the outcome
}.catch {
handle(error: $0)
}
If you return a promise in a then , the next then waits on that promise before
continuing. This is the essence of promises.
Promises are easy to compose, so they encourage you to develop highly
asynchronous apps without fear of the spaghetti code (and associated refactoring
pains) of asynchronous systems that use completion handlers.
APIs That Use Promises
Promises are composable, so return them instead of accepting completion blocks:
class MyRestAPI {
func user() -> Promise<User> {
return firstly {
URLSession.shared.dataTask(.promise, with: url)
}.compactMap {
try JSONSerialization.jsonObject(with: $0.data) as? [String: A
}.map { dict in
User(dict: dict)
}
}
This way, asynchronous chains can cleanly and seamlessly incorporate code from all
over your app without violating architectural boundaries.
Note: We provide promises for Alamofire too!
Background Work
https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md 2/11
22/08/2020 PromiseKit/CommonPatterns.md at master · mxcl/PromiseKit · GitHub
class MyRestAPI {
func avatar() -> Promise<UIImage> {
let bgq = DispatchQueue.global(qos: .userInitiated)
return firstly {
user()
}.then(on: bgq) { user in
URLSession.shared.dataTask(.promise, with: user.imageUrl)
}.compactMap(on: bgq) {
UIImage(data: $0)
}
}
}
All PromiseKit handlers take an on parameter that lets you designate the dispatch
queue on which to run the handler. The default is always the main queue.
PromiseKit is entirely thread safe.
Tip: With caution, you can have all then , map , compactMap , etc., run on a
background queue. See PromiseKit.conf . Note that we suggest only changing
the queue for the map suite of functions, so done and catch will continue to
run on the main queue, which is usually what you want.
Failing Chains
If an error occurs mid-chain, simply throw an error:
firstly {
foo()
}.then { baz in
bar(baz)
}.then { result in
guard !result.isBad else { throw MyError.myIssue }
//…
return doOtherThing()
}
Tip: Swift lets you define an inline enum Error inside the function you are
working on. This isnʼt great coding practice, but it's better than avoiding
throwing an error because you couldn't be bothered to define a good global
Error enum .
func buttonPressed() {
fetch.then { items in
//…
}
}
if fetch.isResolved {
startSpinner()
fetch = API.fetch().ensure {
stopSpinner()
}
}
return fetch
}
With promises, you donʼt need to worry about when your asynchronous operation
finishes. Just act like it already has.
Above, we see that you can call then as many times on a promise as you like. All the
blocks will be executed in the order they were added.
Chaining Sequences
When you have a series of tasks to perform on an array of data:
https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md 4/11
22/08/2020 PromiseKit/CommonPatterns.md at master · mxcl/PromiseKit · GitHub
Note: You usually want when() , since when executes all of its component
promises in parallel and so completes much faster. Use the pattern shown above
in situations where tasks must be run sequentially; animation is a good example.
We also provide when(concurrently:) , which lets you schedule more than one
promise at a time if you need to.
Timeout
let fetches: [Promise<T>] = makeFetches()
let timeout = after(seconds: 4)
Note that if any component promise rejects, the race will reject, too.
Minimum Duration
Sometimes you need a task to take at least a certain amount of time. (For example,
you want to show a progress spinner, but if it shows for less than 0.3 seconds, the UI
appears broken to the user.)
let waitAtLeast = after(seconds: 0.3)
firstly {
foo()
}.then {
waitAtLeast
}.done {
//…
}
The code above works because we create the delay before we do work in foo() . By
the time we get to waiting on that promise, either it will have already timed out or we
will wait for whatever remains of the 0.3 seconds before continuing the chain.
Cancellation
Promises donʼt have a cancel function, but they do support cancellation through a
special error type that conforms to the CancellableError protocol.
func foo() -> (Promise<Void>, cancel: () -> Void) {
let task = Task(…)
var cancelme = false
let cancel = {
cancelme = true
task.cancel()
}
https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md 6/11
22/08/2020 PromiseKit/CommonPatterns.md at master · mxcl/PromiseKit · GitHub
Promises donʼt have a cancel function because you donʼt want code outside of your
control to be able to cancel your operations--unless, of course, you explicitly want to
enable that behavior. In cases where you do want cancellation, the exact way that it
should work will vary depending on how the underlying task supports cancellation.
PromiseKit provides cancellation primitives but no concrete API.
Cancelled chains do not call catch handlers by default. However you can intercept
cancellation if you like:
foo.then {
//…
}.catch(policy: .allErrors) {
// cancelled errors are handled *as well*
}
Important: Canceling a promise chain is not the same as canceling the underlying
asynchronous task. Promises are wrappers around asynchronicity, but they have no
control over the underlying tasks. If you need to cancel an underlying task, you need
to cancel the underlying task!
The library CancellablePromiseKit extends the concept of Promises to fully cover
cancellable tasks.
Retry / Polling
func attempt<T>(maximumRetryCount: Int = 3, delayBeforeRetry: DispatchTime
var attempts = 0
func attempt() -> Promise<T> {
attempts += 1
return body().recover { error -> Promise<T> in
guard attempts < maximumRetryCount else { throw error }
return after(delayBeforeRetry).then(on: nil, attempt)
}
}
return attempt()
}
attempt(maximumRetryCount: 3) {
flakeyTask(parameters: foo)
}.then {
//…
}.catch { _ in
// we attempted three times but still failed
}
In most cases, you should probably supplement the code above so that it re-attempts
only for specific error conditions.
https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md 7/11
Wrapping Delegate Systems
22/08/2020 PromiseKit/CommonPatterns.md at master · mxcl/PromiseKit · GitHub
Be careful with Promises and delegate systems, as they are not always compatible.
Promises complete once, whereas most delegate systems may notify their delegate
many times. This is why, for example, there is no PromiseKit extension for a
UIButton .
A good example of an appropriate time to wrap delegation is when you need a single
CLLocation lookup:
extension CLLocationManager {
static func promise() -> Promise<CLLocation> {
return PMKCLLocationManagerProxy().promise
}
}
init() {
super.init()
retainCycle = self
manager.delegate = self // does not retain hence the `retainCycle`
promise.ensure {
// ensure we break the retain cycle
self.retainCycle = nil
}
}
// use:
CLLocationManager.promise().then { locations in
//…
}.catch { error in
//…
}
https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md 8/11
22/08/2020 PromiseKit/CommonPatterns.md at master · mxcl/PromiseKit · GitHub
Be careful not to ignore all errors, though! Recover only those errors that make sense
to recover.
Promises for Modal View Controllers
class ViewController: UIViewController {
func done() {
dismiss(animated: true)
seal.fulfill(…)
}
}
// use:
ViewController().show(in: self).done {
//…
}.catch { error in
//…
}
This is the best approach we have found, which is a pity as it requires the presentee
to control the presentation and requires the presentee to dismiss itself explicitly.
https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md 9/11
22/08/2020 PromiseKit/CommonPatterns.md at master · mxcl/PromiseKit · GitHub
What if you want access to both username and image in your done ?
The most obvious way is to use nesting:
login().then { username in
fetch(avatar: username).done { image in
// we have access to both `image` and `username`
}
}.done {
// the chain still continues as you'd expect
}
However, such nesting reduces the clarity of the chain. Instead, we could use Swift
tuples:
login().then { username in
fetch(avatar: username).map { ($0, username) }
}.then { image, username in
//…
}
https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md 10/11
22/08/2020 PromiseKit/CommonPatterns.md at master · mxcl/PromiseKit · GitHub
Generally, you don't want this! People ask for it a lot, but usually because they are
trying to ignore errors. What they really need is to use recover on one of the
promises. Errors happen, so they should be handled; you usually don't want to ignore
them.
https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md 11/11