Skip to content
This repository has been archived by the owner on Mar 22, 2019. It is now read-only.

dougzilla32/Cancel

Repository files navigation

PromiseKit Cancel Extensions Build Status

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.

CocoaPods

pod "PromiseKit/Cancel", "~> 6.0"

The extensions are built into PromiseKit.framework thus nothing else is needed.

Carthage

github "PromiseKit/Cancel" ~> 1.0

The extensions are built into their own framework:

// swift
import PromiseKit
import PMKCancel
// objc
@import PromiseKit;
@import PMKCancel;

Examples

  • 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()

Documentation

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()

Extensions

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

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

CancellablePromise<T>
    thenCC<U>(on: DispatchQueue?, body: (T) -> BFTask<U>) -> CancellablePromise 

CoreLocation

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:)

Foundation

NotificationCenter:
    observeCC(once:object:)

Process
	launchCC(_:)

URLSession
	dataTaskCC(_:with:)
	uploadTaskCC(_:with:from:)
	uploadTaskCC(_:with:fromFile:)
	downloadTaskCC(_:with:to:)

MapKit

MKDirections
    calculateCC()
    calculateETACC()
    
MKMapSnapshotter
    startCC()

OMGHTTPURLRQ

URLSession
    GETCC(_ url:query:)
    POSTCC(_ url:formData:)
    POSTCC(_ url:multipartFormData:)
    POSTCC(_ url:json:)
    PUTCC(_ url:json:)
    DELETECC(_ url:)
    PATCHCC(_ url:json:)

StoreKit

SKProductsRequest
    startCC(_:)
    
SKReceiptRefreshRequest
    promiseCC()

SystemConfiguration

SCNetworkReachability
    promiseCC()

UIKit

UIViewPropertyAnimator
	startAnimationCC(_:)

Choose Your Networking Library

All the networking library extensions supported by PromiseKit are now simple to cancel!

Alamofire

// 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()

OMGHTTPURLRQ


// 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
}

Design Goals

  • 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)
    }
}

About

Cancel for PromiseKit extension

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages