PromiseKit 7 adds clear and concise cancellation abilities to promises and to the PromiseKit extensions. Cancelling promises and their associated tasks is now simple and straightforward. Promises and promise chains can safely and efficiently be cancelled from any thread at any time.
UIApplication.shared.isNetworkActivityIndicatorVisible = true
let fetchImage = URLSession.shared.dataTask(.promise, with: url)
.cancellize()
.compactMap{ UIImage(data: $0.data) }
let fetchLocation = CLLocationManager.requestLocation().cancellize().lastValue
let finalizer = firstly {
when(fulfilled: fetchImage, fetchLocation)
}.done { image, location in
self.imageView.image = image
self.label.text = "\(location)"
}.ensure {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch(policy: .allErrors) { error in
// `catch` will be invoked with `PMKError.cancelled` when cancel is called
// on the context. Use the default policy of `.allErrorsExceptCancellation`
// to ignore cancellation errors.
self.show(UIAlertController(for: error), sender: self)
}
//…
// Cancel currently active tasks and reject all cancellable promises
// with 'PMKError.cancelled'. `cancel()` can be called from any thread
// at any time.
finalizer.cancel()
// `finalizer` here refers to the `CancellableFinalizer` for the chain.
// Calling 'cancel' on any promise in the chain or on the finalizer
// cancels the entire chain. Therefore calling `cancel` on the finalizer
// cancels everything.
Promises can be cancelled using a CancellablePromise
. The cancellize()
method on Promise
is used to convert a Promise
into a CancellablePromise
. If a promise chain is initialized with a CancellablePromise
, then the entire chain is cancellable. Calling cancel()
on any promise in the chain cancels the entire chain.
Creating a chain where the entire chain can be cancelled is the recommended usage for cancellable promises.
The CancellablePromise
contains a CancelContext
that keeps track of the tasks and promises for the chain. Promise chains can be cancelled either by calling the cancel()
method on any CancellablePromise
in the chain, or by calling cancel()
on the CancelContext
for the chain. It may be desirable to hold on to the CancelContext
directly rather than a promise so that the promise can be deallocated by ARC when it is resolved.
For example:
let context = firstly {
login()
/* The 'Thenable.cancellize' method initiates a cancellable promise chain by
returning a 'CancellablePromise'. */
}.cancellize().then { creds in
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}.catch(policy: .allErrors) { error in
if error.isCancelled {
// the chain has been cancelled!
}
}.cancelContext
// …
/* Note: Promises can be cancelled using the 'cancel()' method on the 'CancellablePromise'.
However, it may be desirable to hold on to the 'CancelContext' directly rather than a
promise so that the promise can be deallocated by ARC when it is resolved. */
context.cancel()
A CancellablePromise
can be placed at the start of a chain, but it cannot be embedded directly in the middle of a standard (non-cancellable) promise chain. Instead, a partially cancellable promise chain can be used. A partially cancellable chain is not the recommended way to use cancellable promises, although there may be cases where this is useful.
Convert a cancellable chain to a standard chain
CancellablePromise
wraps a delegate Promise
, which can be accessed with the promise
property. The above example can be modified as follows so that once login()
completes, the chain can no longer be cancelled:
/* Here, by calling 'promise.then' rather than 'then' the chain is converted from a cancellable
promise chain to a standard promise chain. In this example, calling 'cancel()' during 'login'
will cancel the chain but calling 'cancel()' during the 'fetch' operation will have no effect: */
let cancellablePromise = firstly {
login().cancellize()
}
cancellablePromise.promise.then {
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}.catch(policy: .allErrors) { error in
if error.isCancelled {
// the chain has been cancelled!
}
}
// …
/* This will cancel the 'login' but will not cancel the 'fetch'. So whether or not the
chain is cancelled depends on how far the chain has progressed. */
cancellablePromise.cancel()
Convert a standard chain to a cancellable chain
A non-cancellable chain can be converted to a cancellable chain in the middle of the chain as follows:
/* In this example, calling 'cancel()' during 'login' will not cancel the login. However,
the chain will be cancelled immediately, and the 'fetch' will not be executed. If 'cancel()'
is called during the 'fetch' then both the 'fetch' itself and the promise chain will be
cancelled immediately. */
let promise = firstly {
login()
}.then {
fetch(avatar: creds.user).cancellize()
}.done { image in
self.imageView = image
}.catch(policy: .allErrors) { error in
if error.isCancelled {
// the chain has been cancelled!
}
}
// …
promise.cancel()
The following classes, methods and functions have been added to PromiseKit to support cancellation. Existing functions or methods with underlying tasks that can be cancelled are indicated by being appended with '.cancellize()'.
Thenable
cancellize(_:) - Converts the Promise or Guarantee (Thenable) into a
CancellablePromise, which is a cancellable variant of the given
Promise or Guarantee (Thenable)
Global functions
after(seconds:).cancellize() - 'after' with seconds can be cancelled
after(_:).cancellize - 'after' with interval can be cancelled
firstly(execute:) - Accepts body returning Promise or CancellablePromise
hang(_:) - Accepts Promise and CancellablePromise
race(_:) - Accepts [Promise] and [CancellablePromise]
when(fulfilled:) - Accepts [Promise] and [CancellablePromise]
when(fulfilled:concurrently:) - Accepts iterator of type Promise or CancellablePromise
when(resolved:) - Accepts [Promise] and [CancellablePromise]
CancellablePromise properties and methods
promise - Delegate Promise for this CancellablePromise
result - The current Result
init(_ bridge:cancelContext:) - Initialize a new cancellable promise bound to the provided Thenable
init(cancellable:resolver body:). - Initialize a new cancellable promise that can be resolved with
the provided '(Resolver) throws -> Void' body
init(cancellable:promise:resolver:) - Initialize a new cancellable promise using the given Promise
and its Resolver
init(cancellable:error:) - Initialize a new rejected cancellable promise
init(cancellable:) - Initializes a new cancellable promise fulfilled with Void
pending() -> (promise:resolver:) - Returns a tuple of a new cancellable pending promise and its
Resolver
CancellableThenable properties and methods
thenable - Delegate Thenable for this CancellableThenable
cancel(error:) - Cancels all members of the promise chain
cancelContext - The CancelContext associated with this CancellableThenable
cancelItemList - Tracks the cancel items for this CancellableThenable
isCancelled - True if all members of the promise chain have been successfully
cancelled, false otherwise
cancelAttempted - True if 'cancel' has been called on the promise chain associated
with this CancellableThenable, false otherwise
cancelledError - The error generated when the promise is cancelled
appendCancellable(cancellable:reject:) - Append the Cancellable task to our cancel context
appendCancelContext(from:) - Append the cancel context associated with 'from' to our
CancelContext
then(on:flags:_ body:) - Accepts body returning CancellableThenable
cancellableThen(on:flags:_ body:) - Accepts body returning Thenable
map(on:flags:_ transform:)
compactMap(on:flags:_ transform:)
done(on:flags:_ body:)
get(on:flags:_ body:)
tap(on:flags:_ body:)
asVoid()
error
isPending
isResolved
isFulfilled
isRejected
value
mapValues(on:flags:_ transform:)
flatMapValues(on:flags:_ transform:)
compactMapValues(on:flags:_ transform:)
thenMap(on:flags:_ transform:) - Accepts transform returning CancellableThenable
cancellableThenMap(on:flags:_ transform:) - Accepts transform returning Thenable
thenFlatMap(on:flags:_ transform:) - Accepts transform returning CancellableThenable
cancellableThenFlatMap(on:flags:_ transform:) - Accepts transform returning Thenable
filterValues(on:flags:_ isIncluded:)
firstValue
lastValue
sortedValues(on:flags:)
CancellableCatchable properties and methods
catchable - Delegate Catchable for this CancellableCatchable
catch(on:flags:policy::_ body:) - Accepts body returning Void
recover(on:flags:policy::_ body:) - Accepts body returning CancellableThenable
cancellableRecover(on:flags:policy::_ body:) - Accepts body returning Thenable
ensure(on:flags:_ body:) - Accepts body returning Void
ensureThen(on:flags:_ body:) - Accepts body returning CancellablePromise
finally(_ body:)
cauterize()
Cancellation support has been added to the PromiseKit extensions, but only where the underlying asynchronous tasks can be cancelled. This example Podfile lists the PromiseKit extensions that support cancellation along with a usage example:
pod "PromiseKit/Alamofire"
# Alamofire.request("http://example.com", method: .get).responseDecodable(DecodableObject.self).cancellize()
pod "PromiseKit/Bolts"
# CancellablePromise(…).then() { _ -> BFTask in /*…*/ } // Returns CancellablePromise
pod "PromiseKit/CoreLocation"
# CLLocationManager.requestLocation().cancellize().then { /*…*/ }
pod "PromiseKit/Foundation"
# URLSession.shared.dataTask(.promise, with: request).cancellize().then { /*…*/ }
pod "PromiseKit/MapKit"
# MKDirections(…).calculate().cancellize().then { /*…*/ }
pod "PromiseKit/OMGHTTPURLRQ"
# URLSession.shared.GET("http://example.com").cancellize().then { /*…*/ }
pod "PromiseKit/StoreKit"
# SKProductsRequest(…).start(.promise).cancellize().then { /*…*/ }
pod "PromiseKit/SystemConfiguration"
# SCNetworkReachability.promise().cancellize().then { /*…*/ }
pod "PromiseKit/UIKit"
# UIViewPropertyAnimator(…).startAnimation(.promise).cancellize().then { /*…*/ }
Here is a complete list of PromiseKit extension methods that support cancellation:
Alamofire.DataRequest
response(_:queue:).cancellize()
responseData(queue:).cancellize()
responseString(queue:).cancellize()
responseJSON(queue:options:).cancellize()
responsePropertyList(queue:options:).cancellize()
responseDecodable(queue::decoder:).cancellize()
responseDecodable(_ type:queue:decoder:).cancellize()
Alamofire.DownloadRequest
response(_:queue:).cancellize()
responseData(queue:).cancellize()
CancellablePromise<T>
then<U>(on: DispatchQueue?, body: (T) -> BFTask<U>) -> CancellablePromise
CLLocationManager
requestLocation(authorizationType:satisfying:).cancellize()
requestAuthorization(type requestedAuthorizationType:).cancellize()
NotificationCenter:
observe(once:object:).cancellize()
NSObject
observe(_:keyPath:).cancellize()
Process
launch(_:).cancellize()
URLSession
dataTask(_:with:).cancellize()
uploadTask(_:with:from:).cancellize()
uploadTask(_:with:fromFile:).cancellize()
downloadTask(_:with:to:).cancellize()
CancellablePromise
validate()
HMPromiseAccessoryBrowser
start(scanInterval:).cancellize()
HMHomeManager
homes().cancellize()
MKDirections
calculate().cancellize()
calculateETA().cancellize()
MKMapSnapshotter
start().cancellize()
SKProductsRequest
start(_:).cancellize()
SKReceiptRefreshRequest
promise().cancellize()
SCNetworkReachability
promise().cancellize()
UIViewPropertyAnimator
startAnimation(_:).cancellize()
All the networking library extensions supported by PromiseKit are now simple to cancel!
// pod 'PromiseKit/Alamofire'
// # https://github.com/PromiseKit/Alamofire
let context = firstly {
Alamofire
.request("http://example.com", method: .post, parameters: params)
.responseDecodable(Foo.self)
}.cancellize().done { foo in
//…
}.catch { error in
//…
}.cancelContext
//…
context.cancel()
And (of course) plain URLSession
from Foundation:
// pod 'PromiseKit/Foundation'
// # https://github.com/PromiseKit/Foundation
let context = firstly {
URLSession.shared.dataTask(.promise, with: try makeUrlRequest())
}.cancellize().map {
try JSONDecoder().decode(Foo.self, with: $0.data)
}.done { foo in
//…
}.catch { error in
//…
}.cancelContext
//…
context.cancel()
func makeUrlRequest() throws -> URLRequest {
var rq = URLRequest(url: url)
rq.httpMethod = "POST"
rq.addValue("application/json", forHTTPHeaderField: "Content-Type")
rq.addValue("application/json", forHTTPHeaderField: "Accept")
rq.httpBody = try JSONSerialization.jsonData(with: obj)
return rq
}
- Provide a streamlined way to cancel a promise chain, which rejects all associated promises and cancels all associated tasks. For example:
let promise = firstly {
login()
}.cancellize().then { creds in // Use the 'cancellize' function to initiate a cancellable promise chain
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}.catch(policy: .allErrors) { error in
if error.isCancelled {
// the chain has been cancelled!
}
}
//…
promise.cancel()
-
Ensure that subsequent code blocks in a promise chain are never called after the chain has been cancelled
-
Fully support concurrency, where all code is thread-safe. Cancellable promises and promise chains can safely and efficiently be cancelled from any thread at any time.
-
Provide cancellable support for all PromiseKit extensions whose native tasks can be cancelled (e.g. Alamofire, Bolts, CoreLocation, Foundation, HealthKit, HomeKit, MapKit, StoreKit, SystemConfiguration, UIKit)
-
Support cancellation for all PromiseKit primitives such as 'after', 'firstly', 'when', 'race'
-
Provide a simple way to make new types of cancellable promises
-
Ensure promise branches are properly cancelled. For example:
import Alamofire
import PromiseKit
func updateWeather(forCity searchName: String) {
refreshButton.startAnimating()
let context = firstly {
getForecast(forCity: searchName)
}.cancellize().done { response in
updateUI(forecast: response)
}.ensure {
refreshButton.stopAnimating()
}.catch { error in
// Cancellation errors are ignored by default
showAlert(error: error)
}.cancelContext
//…
/* **** Cancels EVERYTHING (except... the 'ensure' block always executes regardless)
Note: non-cancellable tasks cannot be interrupted. For example: if 'cancel()' is
called in the middle of 'updateUI()' then the chain will immediately be rejected,
however the 'updateUI' call will complete normally because it is not cancellable.
Its return value (if any) will be discarded. */
context.cancel()
}
func getForecast(forCity name: String) -> CancellablePromise<WeatherInfo> {
return firstly {
Alamofire.request("https://autocomplete.weather.com/\(name)")
.responseDecodable(AutoCompleteCity.self)
}.cancellize().then { city in
Alamofire.request("https://forecast.weather.com/\(city.name)")
.responseDecodable(WeatherResponse.self).cancellize()
}.map { response in
format(response)
}
}