Publishers and subscribers in Combine
Follow how Combine emits and receives values, manages subscriptions, and transforms streams.
Read the Korean original on Velog ↗English translation

Subscribing to a stream of changes creates a different relationship than calling a function whenever a value changes. Publishers and subscribers describe how values are produced, delivered, and completed.
Publishers and subscribers
A Publisher emits values or events from a data source. A Subscriber receives those values and the stream's completion event.
class SubscriberViewModel: ObservableObject {
@Published var count: Int = 0
// Store an AnyCancellable
var timer: AnyCancellable?
init() {
setUpTimer()
}
func setUpTimer() {
// Publisher that emits at regular intervals
timer = Timer
.publish(every: 1, on: .main, in: .common)
.autoconnect()
// Subscriber that receives and handles events
.sink { [weak self] _ in
self?.count += 1
if let count = self?.count, count >= 10 {
// Cancel the subscription and stop its events
self?.timer?.cancel()
}
}
}
}
Cancellable represents work that can be canceled. Calling cancel ends the subscription's work and releases associated resources. AnyCancellable is a type-erased cancellation token that cancels when deinitialized. Store tokens in a Set with store(in:) to keep subscriptions alive and manage their lifetimes, much like a dispose bag in RxSwift.
class SubscriberViewModel: ObservableObject {
@Published var count: Int = 0
// Store cancellation tokens
var cancellables = Set<AnyCancellable>()
init() {
setUpTimer()
}
func setUpTimer() {
Timer
.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
// Unwrap with guard let
guard let self = self else { return }
self.count += 1
if self.count >= 10 {
// Cancel the subscription and stop its events
for item in self.cancellables {
item.cancel()
}
}
}
// Store the cancellation token returned by sink
.store(in: &cancellables)
}
}
maptransforms each incoming value.assignwrites emitted values to a property.- A
[weak self]capture insinkcan prevent a strong reference cycle. debouncewaits for a period without new input before delivering the latest value.
class SubscriberViewModel: ObservableObject {
var cancellables = Set<AnyCancellable>()
@Published var textFieldText: String = ""
@Published var textIsValid: Bool = false
init() {
addTextFieldSubscriber()
}
func addTextFieldSubscriber() {
$textFieldText
// Debounce the latest value
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
// Transform the emitted value
.map { text -> Bool in
if text.count > 3 {
return true
}
return false
}
// Assign published values to a property
// .assign(to: \.textIsValid, on: self)
// Capture self weakly to avoid a reference cycle
.sink(receiveValue: { [weak self] isValid in
self?.textIsValid = isValid
})
.store(in: &cancellables)
}
}
combineLatest combines publishers using the latest value from each, producing a new output when an upstream value changes after all inputs have emitted at least once.
class SubscriberViewModel: ObservableObject {
@Published var count: Int = 0
var cancellables = Set<AnyCancellable>()
@Published var textFieldText: String = ""
@Published var textIsValid: Bool = false
@Published var showButton: Bool = false
init() {
setUpTimer()
addTextFieldSubscriber()
addButtonSubscriber()
}
func setUpTimer() {
Timer
.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
guard let self = self else { return }
self.count += 1
}
.store(in: &cancellables)
}
func addTextFieldSubscriber() {
$textFieldText
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.map { text -> Bool in
if text.count > 3 {
return true
}
return false
}
// .assign(to: \.textIsValid, on: self)
.sink(receiveValue: { [weak self] isValid in
self?.textIsValid = isValid
})
.store(in: &cancellables)
}
func addButtonSubscriber() {
$textIsValid
// Combine two publishers
.combineLatest($count)
.sink { [weak self] isValid, count in
guard let self = self else { return }
if isValid && count >= 10 {
self.showButton = true
} else {
self.showButton = false
}
}
.store(in: &cancellables)
}
}
