← Back to archive

Development · SwiftUI

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

Development archive cover

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)
    }
}
  • map transforms each incoming value.
  • assign writes emitted values to a property.
  • A [weak self] capture in sink can prevent a strong reference cycle.
  • debounce waits 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)
    }
}

← Back to archive