These extensions add clear and concise cancellation abilities to PromiseKit. It is utilized by other PromiseKit extensions that are able to support cancellation. Cancelling promises and their associated tasks is now simple and straightforward.
For example:
UIApplication.shared.isNetworkActivityIndicatorVisible = true
let fetchImage = URLSession.shared.dataTaskCC(.promise, with: url).compactMap{ UIImage(data: $0.data) }
let fetchLocation = CLLocationManager.requestLocationCC().lastValue
let context = 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
/* Will be invoked with a PromiseCancelledError when cancel is called on the context.
Use the default policy of .allErrorsExceptCancellation to ignore cancellation errors. */
self.show(UIAlertController(for: error), sender: self)
}.cancelContext
//…
// Cancel currently active tasks and reject all cancellable promises with PromiseCancelledError
context.cancel()
/* Note: Cancellable promises can be cancelled directly. However by holding on to
the CancelContext rather than a promise, each promise in the chain can be
deallocated by ARC as it is resolved. */
Note: For all code samples, the differences between cancellable promises and standard promises are highlighted in bold.
pod "PromiseKit/Cancel", "~> 6.0"
The extensions are built into PromiseKit.framework
thus nothing else is needed.
github "PromiseKit/Cancel" ~> 1.0
The extensions are built into their own framework:
// swift
import PromiseKit
import PMKCancel
// objc
@import PromiseKit;
@import PMKCancel;
- Cancelling a chain
let promise = firstly {
/* Methods and functions with the CC (a.k.a. cancel chain) suffix initiate a
cancellable promise chain by returning a CancellablePromise. */
loginCC()
}.then { creds in
/* 'fetch' in this example may return either Promise or CancellablePromise --
If 'fetch' returns a CancellablePromise then the fetch task can be cancelled.
If 'fetch' returns a standard Promise then the fetch task cannot be cancelled,
however if cancel is called during the fetch then the promise chain will still be
rejected with a PromiseCancelledError as soon as the 'fetch' task completes.
Note: if 'fetch' returns a CancellablePromise then the convention is to name
it 'fetchCC'. */
fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}.catch(policy: .allErrors) { error in
if error.isCancelled {
// the chain has been cancelled!
}
}
// …
/* 'promise' here refers to the last promise in the chain. Calling 'cancel' on
any promise in the chain cancels the entire chain. Therefore cancelling the
last promise in the chain cancels everything.
Note: 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. */
promise.cancel()
- Mixing Promise and CancellablePromise to cancel some branches and not others
In the example above: if fetch(avatar: creds.user)
returns a standard Promise then the fetch cannot be cancelled. However, if cancel is called in the middle of the fetch then the promise chain will still be rejected with a PromiseCancelledError once the fetch completes. The done
block will not be called and the catch(policy: .allErrors)
block will be called instead.
If fetch
returns a CancellablePromise then the fetch will be cancelled when cancel()
is invoked, and the catch
block will be called immediately.
- Use the 'delegate' promise
CancellablePromise wraps a delegate Promise, which can be accessed with the promise
property. The above example can be modified as follows so that once 'loginCC' completes, the chain cannot be cancelled:
let cancellablePromise = firstly {
loginCC()
}
cancellablePromise.then { creds in
// For this example 'fetch' returns a standard Promise
fetch(avatar: creds.user)
/* Here, by calling 'promise.done' rather than 'done' the chain is converted from a
cancellable promise chain to a standard Promise chain. In this case, calling
'cancel' during the 'fetch' operation has no effect: */
}.promise.done { image in
self.imageView = image
}.catch(policy: .allErrors) { error in
if error.isCancelled {
// the chain has been cancelled!
}
}
// …
cancellablePromise.cancel()
The following classes, methods and functions are part of the Cancel extension. Functions and methods with the CC suffix create a new CancellablePromise, and those without the CC suffix accept an existing CancellablePromise.
Global functions (all returning CancellablePromise unless otherwise noted)
afterCC(seconds:)
afterCC(_ interval:)
firstly(execute body:) // Accepts body returning CancellableTheanble
firstlyCC(execute body:) // Accepts body returning Theanble
hang(_ promise:) // Accepts CancellablePromise
race(_ thenables:) // Accepts CancellableThenable
race(_ guarantees:) // Accepts CancellableGuarantee
raceCC(_ thenables:) // Accepts Theanable
raceCC(_ guarantees:) // Accepts Guarantee
when(fulfilled thenables:) // Accepts CancellableThenable
when(fulfilled promiseIterator:concurrently:) // Accepts CancellablePromise
whenCC(fulfilled thenables:) // Accepts Thenable
whenCC(fulfilled promiseIterator:concurrently:) // Accepts Promise
// These functions return CancellableGuarantee
when(resolved promises:) // Accepts CancellablePromise
when(_ guarantees:) // Accepts CancellableGuarantee
when(guarantees:) // Accepts CancellableGuarantee
whenCC(resolved promises:) // Accepts Promise
whenCC(_ guarantees:) // Accepts Guarantee
whenCC(guarantees:) // Accepts Guarantee
CancellablePromise: CancellableThenable
CancellablePromise.value(_ value:)
init(task:resolver:)
init(task:bridge:)
init(task:error:)
promise // The delegate Promise
result
CancellableGuarantee: CancellableThenable
CancellableGuarantee.value(_ value:)
init(task:resolver:)
init(task:bridge:)
init(task:error:)
guarantee // The delegate Guarantee
result
CancellableThenable
thenable // The delegate Thenable
cancel(error:) // Accepts optional Error to use for cancellation
cancelContext // CancelContext for the cancel chain we are a member of
isCancelled
cancelAttempted
cancelledError
appendCancellableTask(task:reject:)
appendCancelContext(from:)
then(on:_ body:) // Accepts body returning CancellableThenable or Thenable
map(on:_ transform:)
compactMap(on:_ transform:)
done(on:_ body:)
get(on:_ body:)
asVoid()
error
isPending
isResolved
isFulfilled
isRejected
value
mapValues(on:_ transform:)
flatMapValues(on:_ transform:)
compactMapValues(on:_ transform:)
thenMap(on:_ transform:) // Accepts transform returning CancellableThenable or Thenable
thenFlatMap(on:_ transform:) // Accepts transform returning CancellableThenable or Thenable
filterValues(on:_ isIncluded:)
firstValue
lastValue
sortedValues(on:)
CancellableCatchable
catchable // The delegate Catchable
recover(on:policy:_ body:) // Accepts body returning CancellableThenable or Thenable
recover(on:_ body:) // Accepts body returning Void
ensure(on:_ body:)
ensureThen(on:_ body:)
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).responseDecodableCC()(DecodableObject.self)
pod "PromiseKit/Bolts"
# CancellablePromise(…).thenCC() { _ -> BFTask in /*…*/ }
pod "PromiseKit/CoreLocation"
# CLLocationManager.requestLocationCC().then { /*…*/ }
pod "PromiseKit/Foundation"
# URLSession.shared.dataTaskCC()(.promise, with: request).then { /*…*/ }
pod "PromiseKit/MapKit"
# MKDirections(…).calculateCC().then { /*…*/ }
pod "PromiseKit/OMGHTTPURLRQ"
# URLSession.shared.GETCC()("http://example.com").then { /*…*/ }
pod "PromiseKit/StoreKit"
# SKProductsRequest(…).startCC()(.promise).then { /*…*/ }
pod "PromiseKit/SystemConfiguration"
# SCNetworkReachability.promiseCC().then { /*…*/ }
pod "PromiseKit/UIKit"
# UIViewPropertyAnimator(…).startAnimationCC(.promise).then { /*…*/ }
Here is a complete list of PromiseKit extension methods that support cancellation:
Alamofire.DataRequest
responseCC(_:queue:)
responseDataCC(queue:)
responseStringCC(queue:)
responseJSONCC(queue:options:)
responsePropertyListCC(queue:options:)
responseDecodableCC(queue::decoder:)
responseDecodableCC(_ type:queue:decoder:)
Alamofire.DownloadRequest
responseCC(_:queue:)
responseDataCC(queue:)
CancellablePromise<T>
thenCC<U>(on: DispatchQueue?, body: (T) -> BFTask<U>) -> CancellablePromise
CLGeocoder
public func reverseGeocodeCC(location:)
public func geocodeCC(_ addressDictionary:)
public func geocodeCC(_ addressString:)
public func geocodeCC(_ addressString:, region:)
public func geocodePostalAddressCC(_ postalAddress:)
public func geocodePostalAddressCC(_ postalAddress:, preferredLocale:)
CLLocationManager
requestLocationCC(authorizationType:satisfying:)
requestAuthorizationCC(type:)
NotificationCenter:
observeCC(once:object:)
Process
launchCC(_:)
URLSession
dataTaskCC(_:with:)
uploadTaskCC(_:with:from:)
uploadTaskCC(_:with:fromFile:)
downloadTaskCC(_:with:to:)
MKDirections
calculateCC()
calculateETACC()
MKMapSnapshotter
startCC()
URLSession
GETCC(_ url:query:)
POSTCC(_ url:formData:)
POSTCC(_ url:multipartFormData:)
POSTCC(_ url:json:)
PUTCC(_ url:json:)
DELETECC(_ url:)
PATCHCC(_ url:json:)
SKProductsRequest
startCC(_:)
SKReceiptRefreshRequest
promiseCC()
SCNetworkReachability
promiseCC()
UIViewPropertyAnimator
startAnimationCC(_:)
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)
.responseDecodableCC(Foo.self, cancel: context)
}.done { foo in
//…
}.catch { error in
//…
}.cancelContext
//…
context.cancel()
// pod 'PromiseKit/OMGHTTPURLRQ'
// # https://github.com/PromiseKit/OMGHTTPURLRQ
let context = firstly {
URLSession.shared.POSTCC("http://example.com", JSON: params)
}.map {
try JSONDecoder().decoder(Foo.self, with: $0.data)
}.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.dataTaskCC(.promise, with: try makeUrlRequest())
}.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 {
loginCC() // Use CC (a.k.a. cancel chain) methods or CancellablePromise to
// initiate a cancellable promise chain
}.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!
}
}
//…
promise.cancel()
-
Ensure that subsequent code blocks in a promise chain are never called after the chain has been cancelled
-
Fully support concurrecy, where all code is thead-safe
-
Provide cancellable varients for all appropriate PromiseKit extensions (e.g. Foundation, CoreLocation, Alamofire, etc.)
-
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)
}.done { response in
updateUI(forecast: response)
}.ensure {
refreshButton.stopAnimating()
}.catch { error in
// Cancellation errors are ignored by default
showAlert(error: error)
}.cancelContext
//…
// **** Cancels EVERYTHING (however the 'ensure' block always executes regardless)
context.cancel()
}
func getForecast(forCity name: String) -> CancellablePromise<WeatherInfo> {
return firstly {
Alamofire.request("https://autocomplete.weather.com/\(name)")
.responseDecodableCC(AutoCompleteCity.self)
}.then { city in
Alamofire.request("https://forecast.weather.com/\(city.name)")
.responseDecodableCC(WeatherResponse.self)
}.map { response in
format(response)
}
}