API Docs: | CancelForPromiseKit | CPKAlamofire | CPKCoreLocation | CPKFoundation |
---|
CancelForPromiseKit provides clear and concise cancellation abilities for the most excellent PromiseKit and PromiseKit Extensions. While PromiseKit includes basic support for cancellation, CancelForPromiseKit extends this to make cancelling promises and their associated tasks 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: The format for this README and for the project as a whole mirrors PromiseKit in an attempt to be readable and concise. For all code samples, the differences between PromiseKit and CancelForPromiseKit are highlighted in bold.
In your Podfile:
use_frameworks!
target "Change Me!" do
pod "PromiseKit", "~> 6.0"
pod "CancelForPromiseKit", "~> 1.0"
end
CancelForPromiseKit has the same platform and Xcode support as PromiseKit
- 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 functions and methods are part of the core CancelForPromiseKit module. Functions and Methods with the CC suffix create a new CancellablePromise, and those without the CC suffix accept an existing CancellablePromise.
Here is the Jazzy generated CancelForPromiseKit API documentation.
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()
CancelForPromiseKit provides the same extensions and functions as PromiseKit so long as the underlying asynchronous task(s) support cancellation.
The default CocoaPod provides the core cancellable promises and the extension for Foundation. The other extensions are available by specifying additional subspecs in your Podfile
,
eg:
pod "CancelForPromiseKit/MapKit"
# MKDirections().calculateCC().then { /*…*/ }
pod "CancelForPromiseKit/CoreLocation"
# CLLocationManager.requestLocationCC().then { /*…*/ }
As with PromiseKit, all extensions are separate repositories. Here is a complete list of CancelForPromiseKit extensions listing the specific functions that support cancellation (PromiseKit extensions without any functions supporting cancellation are omitted):
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:)
Bolts
Cloudkit
CoreLocation
Foundation
Process
launchCC(_:)
URLSession
dataTaskCC(_:with:)
uploadTaskCC(_:with:from:)
uploadTaskCC(_:with:fromFile:)
downloadTaskCC(_:with:to:)
MapKit
OMGHTTPURLRQ
StoreKit
WatchConnectivity
As with PromiseKit, extensions are optional:
pod "CancelForPromiseKit/CorePromise", "~> 1.0"
Note Carthage installations come with no extensions by default.
All the networking library extensions supported by PromiseKit are now simple to cancel!
// pod 'CancelForPromiseKit/Alamofire'
// # https://github.com/dougzilla32/CancelForPromiseKit-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 'CancelForPromiseKit/OMGHTTPURLRQ'
// # https://github.com/dougzilla32/CancelForPromiseKit-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 'CancelForPromiseKit/Foundation'
// # https://github.com/dougzilla32/CancelForPromiseKit-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
import CancelForPromiseKit
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)
}
}
If you have a question or an issue to report, please use my bug tracker.