Understanding Swift Combine's Future and Deferred Publishers
Swift's Combine framework provides powerful tools for handling asynchronous operations, and two of the most important publishers for single-event scenarios are Future
and Deferred
. Let's explore how these work and when to use each one.
The Future Publisher
The Future
publisher in Combine represents an asynchronous operation that will eventually produce exactly one value before completing or failing. Think of it as Swift's version of a promise from JavaScript or other reactive frameworks.
What makes Future
particularly useful is that it integrates seamlessly with Combine's operator chain, allowing you to transform, filter, and combine it with other publishers effortlessly.
How Future Works
Apple describes Future
as "a publisher that eventually produces a single value and then finishes or fails." In terms of event flow, this looks like a single emission followed by either completion or an error.
Creating a Future Publisher
Let's build a simple Future
that waits 2 seconds before emitting a random integer:
Future<Int, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let number = Int.random(in: 1...10)
promise(.success(number))
}
}
The Future
initializer accepts a closure that receives a "promise" function. This promise function expects a Result
type that matches your Future
's generic parameters. You call this promise with either a success value or a failure when your asynchronous work completes.
For error scenarios, you would call the promise like this:
Future<Int, SomeError> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
promise(.failure(.networkError))
}
}
Future's Immediate Execution
Here's something crucial to understand: Future
begins executing its closure immediately upon creation, regardless of whether anyone has subscribed to it yet.
Let's demonstrate this with some logging:
let futurePublisher = Future<Int, Never> { promise in
print("๐ฎ Future started working")
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let number = Int.random(in: 1...10)
print("๐ฎ Future generated: \(number)")
promise(.success(number))
}
}.print("๐ Event")
Running this code produces:
๐ฎ Future started working
๐ฎ Future generated: 5
Notice that the future begins processing immediately, even without any subscribers. However, without a subscriber, the generated value goes nowhere.
When we add a subscriber:
futurePublisher
.sink { value in
print("๐ง Received: \(value)")
}
.store(in: &cancellables)
Now we see the complete flow:
๐ฎ Future started working
๐ Event: receive subscription: (Future)
๐ Event: request unlimited
๐ฎ Future generated: 5
๐ Event: receive value: (5)
๐ง Received: 5
๐ Event: receive finished
Real-World API Example
Future
excels at wrapping traditional callback-based APIs. Here's how you might wrap a network request:
func fetchPosts(from 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()
}
fetchPosts(from: apiURL)
.sink(
receiveCompletion: { completion in print(completion) },
receiveValue: { posts in print("Loaded \(posts.count) posts") }
)
.store(in: &cancellables)
Remember that calling fetchPosts(from:)
immediately triggers the network request, even if you don't subscribe right away. Sometimes you want to delay this execution until subscription time.
The Deferred Publisher
Deferred
solves the immediate execution problem by wrapping publisher creation in a closure that only runs when someone subscribes. It takes a closure that returns any publisher and calls this closure for each new subscription.
Single Subscriber Scenario
Let's wrap our previous Future
example with Deferred
:
let deferredPublisher = Deferred {
Future<Int, Never> { promise in
print("๐ฎ Future started working")
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let number = Int.random(in: 1...10)
print("๐ฎ Future generated: \(number)")
promise(.success(number))
}
}
.print("๐ Event")
}
With no subscribers, this produces no output at all. The Future
isn't even created until subscription occurs.
Once we add a subscriber:
deferredPublisher
.sink { value in
print("๐ง Received: \(value)")
}
.store(in: &cancellables)
We get the same output as the non-deferred version, but only after subscription.
Multiple Subscribers
Here's where Deferred
shows its unique behavior. With multiple subscribers:
deferredPublisher
.sink { print("๐ง Stream 1: \($0)") }
.store(in: &cancellables)
deferredPublisher
.sink { print("๐ง Stream 2: \($0)") }
.store(in: &cancellables)
The output reveals something interesting:
๐ฎ Future started working
๐ Event: receive subscription: (Future)
๐ Event: request unlimited
๐ฎ Future started working
๐ Event: receive subscription: (Future)
๐ Event: request unlimited
๐ฎ Future generated: 3
๐ Event: receive value: (3)
๐ง Stream 1: 3
๐ Event: receive finished
๐ฎ Future generated: 8
๐ Event: receive value: (8)
๐ง Stream 2: 8
๐ Event: receive finished
Each subscription creates a completely new Future
, resulting in separate random numbers for each subscriber.
Key Takeaways
Future
acts as a promise for single asynchronous events, perfect for wrapping callback-based APIs or one-time operations. However, it begins processing immediately upon creation, which isn't always desirable.
Deferred
provides lazy evaluation by deferring publisher creation until subscription occurs. When combined with Future
, it ensures that expensive operations only happen when someone actually wants the result.
This combination is particularly powerful for API calls, file operations, or any scenario where you want to avoid unnecessary work until a subscriber demonstrates actual demand for the data.
Understanding these publishers helps you build more efficient reactive streams in your Swift applications, ensuring resources are used only when needed while maintaining the clean, composable nature of Combine's publisher chain.