Future and Promise in Swift Combine Framework

Manish Pathak
6 min readJun 26, 2023

--

Why do we need Fututre in combine?

If we talk about modern iOS applications that takes a lot of CPU-intensive tasks. Most operations like network and media file computation must be asynchronous so that the app remains responsive to users. The normal approach to asynchronous programming in Swift is callback-based approach. However, the following caveats apply —

  • Async programming with callbacks is hard to manage.
  • It violates the inversion of control.

Futures and promises have none of these drawbacks.

Let's understand more , All of us have encountered situations where we’ve had to write code like this:

photoManager.getAccess { accessResult in
switch accessResult {
case .success:
photoManager.selectPhoto { selectionResult in
switch selectionResult {
case .success:
photoManager.uploadPhoto { uploadResult in
...
}
case .failure:
...
}
}

case .failure:
photoManager.showAlert {
photoManager.retry {
...
}
}
}
}

This is commonly referred to as the Pyramid of Doom — a code structure that resembles an arrowhead and is considered an anti-pattern resulting from multiple nested closures or structures such as nested conditions or switch statements.

In this material, we will explore how to eliminate nested closures using Combine and thus simplify the code while enhancing its readability and maintainability. Let’s explore how it can be valuable for handling asynchronous tasks, such as networking.

Here is a typical example of running several network requests in sequence:

The callback-based approach scales poorly. Nested pyramid of doom or we call it Callback hell.

Let’s rewrite our example with DataTaskPublisher, which is a special flavor of a future:

On the second benefit, we never deal with nil values explicitly. Conversely, in the previous example, we get triple optionals in the callback.

The new code has a clear improvement in readability.

On the second benefit, we never deal with nil values explicitly. Conversely, in the previous example, we get triple optionals in the callback.

Futures eliminate the entire class of errors related to having nil values or nil errors.

Execute 2 or more requests in parallel —

With callbacks, we would have to resort to higher-level APIs, like Grand Central Dispatch or OperationQueue, and it would still be a non-trivial task.

However, it is straightforward to achieve with futures:

Moreover, not only can we put futures in systematic ways, but we can transform their results, without removing them from the future context.

What is Future ?

Combine provides the Future publisher class to model a single asynchronous event.

Apple’s documentation states that Future is:

A publisher that eventually produces a single value and then finishes or fails.

You can think of a future as a placeholder for a value that doesn’t exist yet. This empty placeholder can happily sit forever, idle in your code. When it finally receives a value, in the context of Combine, the Future is essentially a publisher that will send the fulfilled value down the pipeline.

Future is a context for a value that might not yet exist. Generally, we use a future to represent the eventual completion or failure of an asynchronous operation.

  • Swift comes with a native implementation of futures as a part of the Combine framework:
let future = Future<Int, Never>.init(...)

Future has two types that represent the type of value it wraps and the type of error it can fail with. In our example, these are Int and Never correspondingly.

  • In Combine’s realm, Future is a publisher. Future obeys all publisher’s laws and supports all operations with publishers.

Promise is the eventual result of a future.

We must initialize a future with a promise. Promises are also built into the Combine framework:

let future = Future<Int, Never> { promise in ... }

Promise is essentially a closure that accepts a single Result parameter. This is why Future has a callback-based initializer.

  • Future’ means ‘A publisher that eventually produces a single value and then finishes or fails’ and It provide closure as Future.Promise it has Result type parameter without return value.

Declaration

final class Future<Output, Failure> where Failure: Error

Future is similar with ‘Single’ in RxSwift

We say that we fulfill a future when we pass a value to its promise:

let future = Future<Int, Never> { promise in
promise(.success(1))
}

We say that we reject a future when we pass an error to its promise:

struct DummyError: Error {}

let future = Future<Int, Error> { promise in
promise(.failure(DummyError()))
}

Now that we understand what is a future and a promise, let’s see how we can use them.

How to Implement the Future in Combine?

Basic Usage

To get a value out of a future, we must subscribe to it:

let future = Future<Int, Never> { promise in
promise(.success(1))
}

future.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })

The future is fulfilled with a value 1:

1
finished

We can fulfill a future later by adding some delay:

let future = Future<Int, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
promise(.success(1))
}
}

future.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
print("end")

It will print:

end
1
finished

Future is one-shot, meaning that it finishes immediately after we pass a value or an error to its promise:

let future = Future<Int, Never> { promise in
promise(.success(1))
promise(.success(2))
}

future.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })

It will print:

1
finished

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.

Summary

We use a future to represent an asynchronous computation and we use a promise to deliver a value (or an error) to the future.

Futures and promises lend themselves to delivering a clean and scalable implementation of a compositional asynchronous programming model.

The Swift Combine framework provides native implementation of futures and promises along with a rich set of operations.

--

--