Future
Combine provides the Future
publisher class to model a single asynchronous event. The Future
acts like a promise. It can either fulfil its promise or fail to fulfil its promise. Those familiar with promises from frameworks like PromiseKit might liken Future
to a promise.
The added benefit of using Future
is that it is a publisher, so you can use them with operators and subscribers and easily integrate them into combine data streams.
A Future
emits a single event
Apple’s documentation states that Future
is:
A publisher that eventually produces a single value and then finishes or fails.
In a marble diagram, a single event followed by a completion or an error would look like this.
Create a Future
Let’s create a simple Future
that waits 2 seconds and emits a random number.
Future
is generic over an associated type of output and failure. In this example we will use <Int, Never>
to emit an integer and never fail the stream.
The Future
initialiser takes a closure that takes another closure, (Result<Int, Never>) -> Void
, as its input. We will call this the promise. It is a promise to return a result that matches the generic types of the Future
. We hold onto this promise in our Future
closure and then call the promise with our result or error whenever we have finished our work.
Simple right?
It makes more sense in code. It looks like this.
Future<Int, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let number = Int.random(in: 1...10)
promise(.success(number))
}
}
If we wanted to emit the failure event we simply call the promise like so
Future<Int, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let number = Int.random(in: 1...10)
promise(.failure(.someErrorCase))
}
}
As you can see the Future
initialiser provides you with a promise that you then call with a value or an error to emit something from the Future
publisher.
When does Future
process?
If we change the future publisher back to emitting a success and include some print statements we can look into how and when the future is processing its closures.
let futurePublisher = Future<Int, Never> { promise in
print("๐ฎ Future began processing")
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let number = Int.random(in: 1...10)
print("๐ฎ Future emitted number: \(number)")
promise(Result.success(number))
}
}.print("๐ Publisher event")
Compiling and running this code would create the following logs.
๐ฎ Future began processing
๐ฎ Future emitted number: 2
As you can see, when we create a Future
it immediately starts processing.
There are no logs from .print("๐ Publisher event")
. No downstream subscribers would receive events. This is because there are no subscribers. Even though the Future
is doing its work.
Next, we will add a simple subscriber using sink and print the received value.
let futurePublisher = Future<Int, Never> { promise in
print("๐ฎ Future began processing")
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let number = Int.random(in: 1...10)
print("๐ฎ Future emitted number: \(number)")
promise(Result.success(number))
}
}.print("๐ Publisher event")
futurePublisher
.sink { print ("๐ง Future stream received: \($0)") }
.store(in: &cancellables)
Now the logs look like this.
๐ฎ Future began processing
๐ Publisher event: receive subscription: (Future)
๐ Publisher event: request unlimited
๐ฎ Future emitted number: 3
๐ Publisher event: receive value: (3)
๐ง Future stream received: 3
๐ Publisher event: receive finished
The downstream subscriber clearly receives the emitted event.
API Call Example with Future
Because Future
models a one-time asynchronous event that succeeds or fails it is often used to model an API call or network fetch. Here is how that might look.
func fetch(url: URL) -> AnyPublisher<[Post], Error> {
Future { promise in
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
promise(.failure(error))
return
}
do {
let posts = try JSONDecoder().decode([Post].self, from: data!)
promise(.success(posts))
} catch {
promise(.failure(error))
}
}.resume()
}.eraseToAnyPublisher()
}
fetch(url: url)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
}).store(in: &cancellables)
This code leverages the traditional completion handler of a URLSessionDataTask
to create a Future
. The Future
object is then type erased to an AnyPublisher
for easy use with any downstream subscribers.
Remember that simply calling the fetch(url:)
function will initiate an API call. Therefore, if you are using a function like fetch(url:)
and not immediately subscribing to it, you may want to defer any work that the Future
might do until something subscribes.
Deferred
The Deferred
struct is a simple publisher that takes in a closure as a parameter. That closure must return a DeferredPublisher
. It will call this closure only when a subscriber subscribes. In this way, it defers any publisher processing until a subscriber causes downstream demand for an event.
Deferred
with one subscriber
When we looked at when a future processes we saw that the Future
began its processing as soon as it was created. If we wrap the same code with Deferred
we can see how this defers any processing until a subscription happens.
let deferredPublisher = Deferred {
Deferred {
Future<Int, Never> { promise in
print("๐ฎ Future began processing")
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let number = Int.random(in: 1...10)
print("๐ฎ Future emitted number: \(number)")
promise(Result.success(number))
}
}.print("๐ Publisher event")
}
}
This prints nothing to the console as nothing is subscribed. Deferred
won’t even create the Future
until there is a subscription.
Let’s add a subscriber.
let deferredPublisher = Deferred {
Deferred {
Future<Int, Never> { promise in
print("๐ฎ Future began processing")
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let number = Int.random(in: 1...10)
print("๐ฎ Future emitted number: \(number)")
promise(Result.success(number))
}
}.print("๐ Publisher event")
}
}
deferredPublisher
.sink { print ("๐ง Future stream received: \($0)") }
.store(in: &cancellables)
Now we get the same logs as when we created the Future
without Deferred
, but only because we have a definitive subscriber.
๐ฎ Future began processing
๐ Publisher event: receive subscription: (Future)
๐ Publisher event: request unlimited
๐ฎ Future emitted number: 7
๐ Publisher event: receive value: (7)
๐ง Future stream received: 7
๐ Publisher event: receive finished
Deferred
with multiple subscribers
Let’s subscribe two subscribers to the same deferred publisher.
deferredPublisher
.sink { print ("๐ง Future stream 1 received: \($0)") }
.store(in: &cancellables)
deferredPublisher
.sink { print ("๐ง Future stream 2 received: \($0)") }
.store(in: &cancellables)
Here are the logs we get now.
๐ฎ Future began processing
๐ Publisher event: receive subscription: (Future)
๐ Publisher event: request unlimited
๐ฎ Future began processing
๐ Publisher event: receive subscription: (Future)
๐ Publisher event: request unlimited
๐ฎ Future emitted number: 9
๐ Publisher event: receive value: (9)
๐ง Future stream 1 received: 9
๐ Publisher event: receive finished
๐ฎ Future emitted number: 6
๐ Publisher event: receive value: (6)
๐ง Future stream 2 received: 6
๐ Publisher event: receive finished
Notice that the emitted events are different.
Each subscription makes the DeferredPublisher
run its closure that creates a Future
. Therefore each subscription gets a different Future
and therefore a different emitted event.
Conclusion
Future
acts like a promise to return some value in the future. It can also fail. It’s useful for creating publishers of one-time asynchronous events. We need to be conscious that Future
does its processing as soon as it is created.
Deferred
wraps the creation of any type of publisher and only creates the publisher when a subscriber is present. Used with Future
deferred can defer the Future
’s work until downstream subscribers are present.