Mastering the Power of Subjects in Combine: A Comprehensive Guide — Part 5
A comprehensive understanding of how subjects work in the Combine framework.
- Introduction to Subjects in Combine: An Overview
- Understanding CurrentValueSubject: Definition and Usage
- Implementing CurrentValueSubject in Your iOS App: Step-by-Step Guide
- Exploring PassthroughSubject: Definition and Practical Applications
- How to Use PassthroughSubject in Your iOS App: Best Practices
- Understanding the lifecycle of a subject
- A Comparison between PassthroughSubject vs CurrentValueSubject
Introduction to Subjects

In the context of the Combine framework, subjects are a type of object used to both produce and consume values. They are essentially a bridge between publishers and subscribers.
There are two types of built-in subjects with Combine:
- CurrentValueSubject: A subject that holds a current value and emits it to new subscribers immediately upon subscription.
- PassthroughSubject: A subject that does not hold a current value, but simply passes along any values it receives to its subscribers.
Subjects are often used as a way to bridge the gap between imperative and reactive programming styles. They can be used to encapsulate mutable states in a reactive pipeline, allowing you to interact with it imperatively while still maintaining the benefits of a reactive architecture.
Both CurrentValueSubject
and PassthroughSubject
are also useful for creating publishers for objects conforming to ObservableObject
. This protocol is supported by a number of declarative components within SwiftUI.
Understanding CurrentValueSubject
CurrentValueSubject is a type of subject in the Combine framework that represents a publisher and subscriber that maintains a current value. It can hold a single value and publish new values to any subscribers whenever a new value is set.
When you create a CurrentValueSubject, you specify an initial value that it should hold. Any new subscribers that subscribe to the subject will immediately receive the current value. Subscribers will continue to receive new values as they are published to the subject.
You can also use the send() method to publish new values to the subject. When a new value is sent, the subject will update its current value and publish the new value to all subscribers.
CurrentValueSubject is useful when you need to maintain a current value that can be queried at any time by subscribers. For example, you might use a CurrentValueSubject to represent the current user’s authentication status in your app, so that any part of the app can query the subject to determine whether the user is currently authenticated.
It’s important to note that CurrentValueSubject can only hold a single value at a time. If you need to publish multiple values over time, you should use another type of subject, such as PassthroughSubject.
Implementing CurrentValueSubject
A CurrentValueSubject is initialized with an initial value. Unlike with a PassthroughSubject, new subscribers will receive this initial value upon subscribing. In the following example, we’ve created an Uploader that initially holds the pending state:
struct Uploader {
enum State {
case pending, uploading, finished
}
enum Error: Swift.Error {
case uploadFailed
}
let subject = CurrentValueSubject<State, Error>(.pending)
func startUpload() {
subject.send(.uploading)
}
func finishUpload() {
subject.value = .finished
subject.send(completion: .finished)
}
func failUpload() {
subject.send(completion: .failure(.uploadFailed))
}
}
The value of a CurrentValueSubject can be set using the send method or by directly assigning it to the value property. The subject can also be finished, either successfully or with an error.
Implementors can subscribe to events using the sink method:
let uploader = Uploader()
uploader.subject.sink { completion in
switch completion {
case .finished:
print("Received finished")
case .failure(let error):
print("Received error: (error)")
}
} receiveValue: { message in
print("Received message: (message)")
}
Which, in case of a successful upload, print out all different states:
uploader.startUpload()
uploader.finishUpload()
// Received message: pending
// Received message: uploading
// Received message: finished
// Received finished
The pending state is the initial state set and is received directly upon subscribing. Once the upload completes, the final finished state is passed through, and the stream is closed through a finished event.
In case of a failed upload, the stream will be finished with an error:
uploader.startUpload()
uploader.failUpload()
// Received message: pending
// Received message: uploading
// Received error: uploadFailed
After either a regular finished event or a failure, the subject will pass no more values. This is due to the lifecycle of a subject.
Exploring PassthroughSubject
PassthroughSubject is a type of subject in the Combine framework that acts as a publisher and subscriber, passing along any values it receives to its subscribers. Unlike CurrentValueSubject, PassthroughSubject does not hold a current value.
When you create a PassthroughSubject, you specify the type of values that it will publish. You can use the send() method to publish new values to the subject, and any subscribers will receive the new values.
PassthroughSubject is useful when you need to publish values that are generated outside the subject, such as user input events. You can use a PassthroughSubject to capture those events and publish them to any subscribers that need to react to them.
It’s important to note that PassthroughSubject does not buffer values that are sent when no subscribers are listening. If a value is sent and there are no subscribers, the value is simply discarded. If you need to ensure that subscribers receive all values, even those sent before they subscribe, you should use a type of subject that does buffer values, such as ReplaySubject or BehaviorSubject.
How to Use PassthroughSubject
A PassthroughSubject is initialized without any value. In the following example, we’re simulating a chatroom that can be closed manually or by an occurring error like a missing network connection.
struct ChatRoom {
enum Error: Swift.Error {
case missingConnection
}
let subject = PassthroughSubject<String, Error>()
func simulateMessage() {
subject.send("Hello!")
}
func simulateNetworkError() {
subject.send(completion: .failure(.missingConnection))
}
func closeRoom() {
subject.send("Chat room closed")
subject.send(completion: .finished)
}
}
You can use the subject to subscribe to received messages from the chatbox:
let chatRoom = ChatRoom()
chatRoom.subject.sink { completion in
switch completion {
case .finished:
print("Received finished")
case .failure(let error):
print("Received error: (error)")
}
} receiveValue: { message in
print("Received message: (message)")
}
In a happy flow, the room would receive some messages and gets closed manually:
chatRoom.simulateMessage()
chatRoom.closeRoom()
// Received message: Hello!
// Received message: Chat room closed
// Received finished
However, it’s also possible that a network error is received. In this case, the subject is completed using an error:
chatRoom.simulateMessage()
chatRoom.simulateNetworkError()
// Received message: Hello!
// Received error: missingConnection
Understanding the lifecycle of a subject
It’s important to understand the lifecycle of a subject. Whenever a finished event is received, the subject will no longer pass through any new values.
This can be demonstrated with the above example by trying to send a message after a network error occurred:
chatRoom.simulateMessage()
chatRoom.simulateNetworkError()
chatRoom.simulateMessage()
// Received message: Hello!
// Received error: missingConnection
The last message is no longer printed as the subscription is already closed due to the finished error event.
When using either a PassthroughSubject or CurrentValueSubject, it’s important to think about the lifecycle and close a subject when it’s clear that there are no values to be expected anymore.
A Comparison between PassthroughSubject vs CurrentValueSubject
PassthroughSubject and CurrentValueSubject are two types from the Combine framework that conforms to the Subject
protocol. Both are very similar but have a few keys differences that are important to know. You can see them as custom publishers, but the main difference is that they are a little easier to work with.
The main difference between both subjects is that a PassthroughSubject doesn’t have an initial value or a reference to the most recently published element. Therefore, new subscribers will only receive newly emitted events.
Both subjects are explained well by using an analogy:
- A PassthroughSubject is like a doorbell push button
When someone rings the bell, you’re only notified when you’re at home - A CurrentValueSubject is like a light switch
When a light is turned on while you’re away, you’ll still notice it was turned on when you get back home.