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

CurrentValueSubjectPassthroughSubject
StatefulMore stateless
Requires an initial valueDoes not need an initial value
Stores most recently published valueDoes not store current value
Emits events and stores its valueOnly emits events to subscribers
Can access the current value through .valueCannot 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.