Understanding PassthroughSubject vs CurrentValueSubject in Combine

Ghoshit VoraGhoshit Vora
6 min read

In the modern iOS development landscape, Combine has become an essential tool for handling asynchronous and event-driven code. If you're diving into reactive programming with Swift, understanding the differences between PassthroughSubject and CurrentValueSubject is crucial to avoid unwanted behavior and memory pitfalls.

This article walks you through what Combine is, explains Publisher, Subscriber, and Subject in detail, and finally compares PassthroughSubject and CurrentValueSubject with complete SwiftUI examples for each.


☁️ What is Combine?

Introduced in iOS 13, Combine is Apple's declarative Swift framework for managing asynchronous events and data streams. It provides a unified approach to handle various asynchronous operations, such as user inputs, network responses, and timer events, by allowing you to define pipelines of operations on values over time. Built around Publishers, Subscribers, and Operators, Combine enables you to model data as streams, simplifying complex asynchronous logic without the need for deeply nested callbacks or external libraries like RxSwift. This leads to more predictable and readable code when dealing with event-driven programming and data flow within your application.


🔁 Combine Basics: Publisher, Subscriber & Subject

In Combine, the core building blocks are Publishers, Subscribers, and Subjects. Here's how they work using simple login-related examples to make the concept crystal clear:

🔹 Publisher

A Publisher is a type that emits a sequence of values over time. It defines how values are produced and when they are delivered to subscribers. You can think of it as a data stream—like text input, network responses, or system notifications.

Example – A static publisher for a pre-filled email:

import Combine

let emailPublisher = Just("user@example.com")
emailPublisher.sink { email in
   print("Pre-filled email is:", email)
}

🧠 Explanation:
Just is a simple publisher that emits a single value ("user@example.com") and then finishes. This could simulate a situation where you autofill a user’s last used email.

🖨️ Output:

Pre-filled email is: user@example.com

🔹 Subscriber

A Subscriber receives values emitted by a Publisher. It defines how to handle received values, errors, and completion. The most common subscriber is the .sink() operator, which lets you attach custom logic to handle updates.

Example – Subscribing to password strength:

import Combine

let passwords = ["123", "password123", "strongPassword!"].publisher
passwords.sink { password in
   print("User entered password:", password)
}

🧠 Explanation:
We create a publisher from an array of passwords and subscribe using .sink. Each password simulates a user typing a different input.

🖨️ Output:

User entered password: 123  
User entered password: password123  
User entered password: strongPassword!

🔹 Subject

A Subject in Combine uniquely functions as both a Publisher and a Subscriber, allowing you to manually send new values into its stream, effectively bridging imperative code like UI interactions into reactive pipelines. Combine offers two key Subject types: PassthroughSubject and CurrentValueSubject, both enabling the broadcasting of custom events to multiple subscribers, which is particularly useful in architectures like MVVM. Let's dive into its types one by one.

📌 PassthroughSubject

A PassthroughSubject broadcasts values only to current subscribers and doesn't remember past emissions. Ideal for one-time events (button taps, triggers) where you only care about what's happening now. Notably, it does not have an initial value.

Example:

import Combine

let eventSubject = PassthroughSubject<String, Never>()

eventSubject.sink { value in
    print("Subscriber received: \(value)")
}

eventSubject.send("Button Tapped")

🧠 Explanation:

In this example, we first create a PassthroughSubject named eventSubject. This subject is designed to emit String values and, in this case, never fails (indicated by Never for the failure type).

Next, we establish a subscriber to eventSubject using the sink operator. The closure provided to sink will be executed every time a new value is emitted by the eventSubject. Inside the closure, we simply print the received value to the console.

Finally, we manually send the string "Button Tapped" to the eventSubject using the send() method. This action triggers the execution of the subscriber's closure.

🖨️ Output:

Subscriber received: Button Tapped

As you can see in the output, when we called eventSubject.send("Button Tapped"), the subscriber we attached earlier received this value, and the print statement within its closure was executed, outputting "Subscriber received: Button Tapped". Because PassthroughSubject doesn't store past values, any subscribers attached after this send() call would not receive this particular "Button Tapped" event.

📌 CurrentValueSubject

A CurrentValueSubject stores the latest value and gives it to new subscribers immediately. Best for managing state (form fields, settings) where you need the current value available. Crucially, it requires an initial value and remembers the last one.

Example:

import Combine

let stateSubject = CurrentValueSubject<String, Never>("Initial")

stateSubject.send("Updated")

stateSubject.sink { value in
    print("New subscriber got: \(value)")
}

🧠 Explanation:

In this example, we create a CurrentValueSubject named stateSubject. Importantly, we initialize it with the value "Initial". This means that upon creation, stateSubject holds this initial string.

Next, we send a new value, "Updated", to the stateSubject. This updates the currently held value of the subject.

Finally, we attach a subscriber to stateSubject using the sink operator. The closure provided to sink will receive the current value of the stateSubject immediately upon subscription and any subsequent values that are sent.

🖨️ Output:

New subscriber got: Updated

As you can see in the output, the subscriber immediately received "Updated". This is because CurrentValueSubject stores the latest value. Even though the "Updated" value was sent before the subscriber was attached, the new subscriber still received the most recent value held by the stateSubject. If more values were sent to stateSubject after this point, the subscriber would receive those as well. The initial value "Initial" was not received by this subscriber because the subscription happened after it was superseded by "Updated".

🧠 When Should You Use Each Subject?

When choosing between PassthroughSubject and CurrentValueSubject, the key is understanding intent and state requirements.

Use PassthroughSubject when you're broadcasting events that don’t require historical context. This is perfect for gesture events, button taps, or one-time actions.

Use CurrentValueSubject when you're working with stateful data. For instance, when handling form inputs, user preferences, or selections that must be retained and observed—even by late subscribers.

From a design perspective:

  • PassthroughSubject is ephemeral.

  • CurrentValueSubject is persistent.

Choosing the right subject not only affects behavior but helps in building robust and predictable MVVM systems using Combine.

📌 Conclusion

Understanding the difference between PassthroughSubject and CurrentValueSubject is essential to mastering reactive programming in Combine. In SwiftUI, these subjects help your views respond seamlessly to user actions and state changes, making your app feel dynamic and responsive.

Choosing the right subject in the right scenario leads to cleaner code, better state management, and a more predictable architecture—especially when dealing with user-driven workflows like login, search, or form updates.

🧩 You can explore the full source code examples for both PassthroughSubject and CurrentValueSubject in this GitHub repository:
🔗 https://github.com/ghoshitvora/CombineSubjectExample

Lastly, if you have feedback, suggestions, or notice anything that needs correcting, please don’t hesitate to let me know — I’d love to hear your thoughts!

0
Subscribe to my newsletter

Read articles from Ghoshit Vora directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ghoshit Vora
Ghoshit Vora

Senior iOS Developer with a strong background in building scalable and maintainable mobile applications using Swift, SwiftUI, and UIKit. Skilled in architecture patterns like MVVM, Clean Architecture, and Protocol-Oriented Programming. I focus on writing clean, testable code, implementing responsive UI, and optimizing app performance while following SOLID principles and leveraging Combine, Swift Concurrency, and Core Data in modern iOS development.