Futures Than Can Fail
-
A type for managing some work that may either succeed or fail at some point in the future, including interacting with the end result of the work.
Tasks work exactly like futures but, because they operate on types that have one or more exclusive states to describe a success or failure, have patterns for dealing with just the successful value, the failing error, chaining dependent operations together, recovering from errors, and so on.
Returning a
Task
from your API encapsulates the entire lifecycle of asynchronous work, regardless of it being managed byOperationQueue
, orDispatchQueue
, orURLSession
. Consumers of yourTask
object might interact with the task by cancelling, pausing, or resuming it.Creating a Task
Like
Future
, a task will forward operations involving the result of the work being performed to some underlying type, hiding the implementation details of your asynchronous API.let promise = Task<Int>.Promise() DispatchQueue.any().asyncAfter(deadline: .now() + 3) { if Bool.random() { promise.succeed(with: 4) // chosen by fair dice roll. // guaranteed to be random. } else { promise.fail(with: Error.reallyBad) } } return Task(promise)
Tasks Represent Workflows
You can design an API to use
Task
even the result is known immediately. This allows you to evolve your code over time without changing callers of the code.let alreadySucceeded = Task(success: "13 miles away") let alreadyFailed = Task(failure: Error.couldNotFetchLocation)
Consider a method that checks the validity of parameters to a web service before fetching from it:
func fetchFriends(for user: User) throws -> Future<[Friend]?> { guard !user.id.isEmpty else { throw Error.invalidParameters } ... }
Consuming this asynchronous value must be done in multiple paths:
do { let futureFriends = try fetchFriends(for: currentUser) futureFriends.upon(managedObjectContext) { (friends) in if let friends = friends { do { try import(friends) catch { // handle an error } } else { // handle an error } } } catch { // handle an error }
Embracing
Task
as the common currency type between layers of code in your application can consolidate these multiple branches of checking.func fetchFriends(for user: User) -> Task<[Friend]> { guard !user.id.isEmpty else { return Task(failure: Error.invalidParameters) } ... } fetchFriends(for: currentUser) .map(upon: managedObjectContext, transform: import) .uponSuccess(on: .main) { _ in /* handle success */ } .uponFailure(on: .main) { _ in /* handle failure */ }
Tasks Can Be Cancelled
When creating a task from a future or promise, an optional
cancellation
handler can be provided. Inside this method body, you can cancel ongoing work, such as a network connection or image processing.When the underlying work can be interrupted, cancelling a
Task
will typically lead to the operation completing with an error, such asCocoaError.userCancelled
.let promise = Task<UIImage>.Promise() let operation = makeImageProcessingOperation(for: data) operation.completionBlock = { [unowned operation] in promise.fill(with: operation.result!) } operationQueue.add(operation) return Task(promise, uponCancel: operation.cancel)
Cancellation may be invoked in any threading context. If work associated with cancellation must be done on a specific queue, dispatch to that queue from within the cancellation handler.
Tasks Can Report Progress
On macOS, iOS, watchOS, and tvOS, where apps are expected to react to current conditions,
Task
will automatically maintain instances of theProgress
object. These objects can be used to drive UI controls displaying that progress, as well as interactive controls like buttons to cancel, pause, or resume the work.In the simplest cases, using
Task
and methods likemap
andandThen
give some insight into the work your application is doing and improve the user experience. Consider:let task = downloadImage(for: url) .map(upon: .any(), transform: decompressImage) .map(upon: .any(), start: applyFiltersToImage) .map(upon: .any(), start: writeImageToCache)
task.progress
will have four work units, each of which will complete after the work assiociated with the initial task,map
,andThen
, andmap
, yielding progress updates of 25%, 50%, 75%, and 100% respectively.If you have a better source of data for progress, like those provided on
URLSessionTask
orUIDocument
, those can also be incorporated intoTask
during creation. TheseTask
s are weighted more than surrounding calls tomap
orandThen
. For instance, you can modifydownloadImage
above to include another source of progress:func downloadImage(for url: URL) -> Task<Data> { let promise = Task<Data>.Promise() let urlSessionTask = urlSession.dataTask(with: url) { promise.fill(with: ...) } urlSessionTask.resume() return Task(promise, progress: urlSessionTask.progress) }
downloadImage
will account for up to 90% of the returned progress. That 90%slice
will fill in as chunks of data get loaded from the network.You may also create your own
Progress
instances to be given toTask
. Progress objects use any context you desire, like byte counts or fractions, and will also be weighted higher byTask
. IfapplyFiltersToImage
above applies 5 user-selected filters, you might create a custom progress and update it when each of the filters is complete:func applyFiltersToImage(_ image: UIImage) -> UIImage { var image = image let progress = Progress(totalUnitCount: filters.count) for (n, filter) in filters.enumerated() { image = filter.apply(to: image) progress.completedUnitCount = n } return image }
downloadImage
andapplyFiltersToImage
will each take up to 45% of the returned progress.Seealso
TaskProtocol
Seealso
Future
Declaration
Swift
public final class Task<Success>
-
An interface describing a thread-safe way for interacting with the result of some work that may either succeed or fail at some point in the future.
A
task
is a superset of a future where the asynchronously-determined value represents 1 or more exclusive states. These states can be abstracted over by extensions on the task protocol. The future value is almost always theTaskResult
type, but many types conforming toTaskProtocol
may exist.Seealso
FutureProtocol
Declaration
Swift
public protocol TaskProtocol : FutureProtocol where Self.Value : Either
-
A type that can exclusively represent one of two values.
By design, an either is symmetrical and treats its variants the same. For representing the most common case of success and failures, prefer a result type like
TaskResult
.This protocol describes a minimal interface for representing a result type to overcome limitations with Swift. It is expected that it will be removed completely at some later point.
See moreDeclaration
Swift
@available(swift, deprecated: 100000) public protocol Either