In this blog post, we explore the concept of subjects in the Combine framework. We’ll look at two types provided by Apple: CurrentValueSubject
and PassthroughSubject
. We’ll explore their differences and when to use them.
What are Subjects?
Subject
is a type of publisher in the Combine framework that adheres to the Subject
protocol. It has a .send(_:)
method that allows developers to send specific values to a subscriber.
Subjects put values into a stream. This is useful when you need to mix imperative code with Combine.
A Subject
can broadcast values to many subscribers. It is often used to connect or cascade many pipelines together.
There are two built-in Subject types in Combine: CurrentValueSubject
and PassthroughSubject
. CurrentValueSubject
requires an initial state and remembers its values, while PassthroughSubject
does not. Both provide updated values to subscribers when .send()
is invoked.
CurrentValueSubject
We must initialise CurrentValueSubject
with a value. It stores this value as its current value, like the name suggests. Calling send()
on the subject changes the value and emits an event.
Here’s a short example.
let currentValueSubject = CurrentValueSubject<Int, Never>(1)
currentValueSubject.sink { int in
print("first subscriber got \(int)")
}.store(in: &cancellables)
currentValueSubject.send(2)
currentValueSubject.sink { int in
print("second subscriber got \(int)")
}.store(in: &cancellables)
currentValueSubject.send(3)
The above code would create these logs.
first subscriber got 1
first subscriber got 2
second subscriber got 2
second subscriber got 3
first subscriber got 3
PassthroughSubject
PassthroughSubject
is not initialised with a value. As the name suggest, it passes events through when we call .send()
.
Here’s a short example.
let passthroughSubject = PassthroughSubject<Int, Never>()
passthroughSubject { int in
print("first subscriber got \(int)")
}.store(in: &cancellables)
passthroughSubject(2)
passthroughSubject { int in
print("second subscriber got \(int)")
}.store(in: &cancellables)
passthroughSubject(3)
The above code would create these logs.
first subscriber got 2
second subscriber got 3
first subscriber got 3
Notice how the second subscriber received only the 3. This is because the second subscriber subscribed after the subject emitted 2.
CurrentValueSubject compared to PassthroughSubject
CurrentValueSubject | PassthroughSubject |
---|---|
Stateful | More stateless |
Requires an initial value | Does not need an initial value |
Stores most recently published value | Does not store current value |
Emits events and stores its value | Only emits events to subscribers |
Can access the current value through .value | Cannot access a value |
When to Use or Not Use Subjects
Using a PassthroughSubject
to test
PassthroughSubject
is often used in testing. You can inject it as publisher then send events as needed.
In the following example we inject a passthrough subject into a function that outputs integers divisible by three. Then we subscribe to the functions output. Now we can send whatever events we want via the passthrough subject to test the function.
func test_combine() {
let passthroughSubject = PassthroughSubject<Int, Never>()
let divisibleByThree = divisibleByThree(input: passthroughSubject.eraseToAnyPublisher())
let expectation = XCTestExpectation()
divisibleByThree
.collect(3)
.sink { ints in
XCTAssertEqual([3,6,9], ints)
expectation.fulfill()
}.store(in: &cancellables)
passthroughSubject.send(1)
passthroughSubject.send(2)
passthroughSubject.send(3)
passthroughSubject.send(6)
passthroughSubject.send(9)
passthroughSubject.send(10)
wait(for: [expectation], timeout: 1)
}
Bridging Imperative Code
Subjects are often used to bridge existing imperative code to Combine. In a previous post I discussed how to emit text events from a UISearchBar. One of the examples showed how to conform to the UISearchBarDelegate
protocol, implement the textDidChange
method, then send events through a PassthroughSubject
called textSubject
.
extension ViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
textSubject.send(searchBar.text ?? "")
}
}
This is a typical use case for subjects. Bridging imperative APIs to the Combine world.
When to avoid Subjects? (Hint: most of the time)
Dave Sexton has a fantastic and detailed post on when to use subjects in the ReactiveX world. It’s well worth a read. Swap “observable” for “publisher” or “subscriber” though.
My oversimplified summary of his post is:
Avoid subjects if you can
The more we use subject the less clear our data streams become.
Here’s an example I’ve seen before but we want to avoid:
final class ViewModel {
var cancellables: Set<AnyCancellable> = []
let currentValueSubject = CurrentValueSubject<String?, Never>("Placeholder")
init() {
API.getUserID()
.sink { id in
self.currentValueSubject.send(id)
}.store(in: &cancellables)
}
}
There is no need to create a subject here when you can assign
the stream to a publisher.
final class ViewModel {
var cancellables: Set<AnyCancellable> = []
@Published var text: String? = "Placeholder"
init() {
API
.getUserID()
.assign(to: \.text, on: self)
.store(in: &cancellables)
}
}
Or we can be even more succinct, functional and testable. We can make our view model a function that takes a function. It will be easy to test with a fake API function. In production it can use the default API code.
func viewModel(api: (URL) -> AnyPublisher<String?, Never> = API.getUserID) -> AnyPublisher<String?, Never> {
api(API.users)
}
Conclusion
Subjects in Combine are a powerful tool for managing and manipulating data streams. CurrentValueSubject
and PassthroughSubject
have their differences, and should be used accordingly. We’d like to avoid subjects, but they can be useful for bridging imperative code or testing.