Introduction

In Combine, a publisher only emits values while its subscription is active. If the AnyCancellable that holds the subscription is deallocated, the subscription is cancelled and no more values are received. This is a common source of bugs where publishers appear to "stop working" because the cancellable was not stored properly.

Symptoms

  • Publisher emits initial values then stops
  • sink or assign closure is never called after first emission
  • AnyCancellable stored in a local variable goes out of scope
  • Network requests cancel before response arrives
  • Timer publishers stop firing after a few iterations

Example problem: ``swift func setupPublisher() { // PROBLEM: cancellable is a local variable, deallocated when function returns let cancellable = Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() .sink { date in print("Timer fired: \(date)") // Fires once, then stops } }

Common Causes

  • AnyCancellable stored in local variable instead of property or Set
  • ViewModel deallocated because nothing holds a strong reference to it
  • store(in:) on a Set that is itself deallocated
  • Using .sink without storing the returned AnyCancellable
  • SwiftUI view recreation causing subscription loss

Step-by-Step Fix

  1. 1.Store cancellables in a Set property:
  2. 2.```swift
  3. 3.class ViewModel: ObservableObject {
  4. 4.@Published var items: [Item] = []
  5. 5.private var cancellables = Set<AnyCancellable>()

func setup() { apiClient.fetchItems() .receive(on: DispatchQueue.main) .sink( receiveCompletion: { completion in if case .failure(let error) = completion { print("Error: \(error)") } }, receiveValue: { [weak self] items in self?.items = items } ) .store(in: &cancellables) } } ```

  1. 1.Use property wrapper for automatic cancellation:
  2. 2.```swift
  3. 3.@propertyWrapper
  4. 4.struct AutoCancellable {
  5. 5.private var cancellables = Set<AnyCancellable>()

var wrappedValue: Set<AnyCancellable> { get { cancellables } set { cancellables = newValue } }

// Automatically clears on deinit }

// Or use a simpler pattern with a base class: class CancellableHolder { var cancellables = Set<AnyCancellable>()

deinit { cancellables.removeAll() } } ```

  1. 1.Debug cancelled subscriptions:
  2. 2.```swift
  3. 3.extension Publisher {
  4. 4.func logLifecycle() -> Publishers.HandleEvents<Self> {
  5. 5.self.handleEvents(
  6. 6.receiveSubscription: { sub in
  7. 7.print("Subscribed: \(sub)")
  8. 8.},
  9. 9.receiveCancel: {
  10. 10.print("Cancelled at: \(Thread.callStackSymbols.prefix(5))")
  11. 11.}
  12. 12.)
  13. 13.}
  14. 14.}

// Usage apiClient.fetchData() .logLifecycle() .sink { ... } .store(in: &cancellables) ```

  1. 1.Handle SwiftUI view lifecycle correctly:
  2. 2.```swift
  3. 3.struct ContentView: View {
  4. 4.@StateObject private var viewModel = ViewModel()
  5. 5.// @StateObject keeps viewModel alive as long as the view exists

var body: some View { List(viewModel.items) { item in Text(item.title) } .onAppear { viewModel.setup() // Setup on appear } } } ```

Prevention

  • Always use .store(in: &cancellables) with a Set property
  • Use @StateObject for ViewModels in SwiftUI, not @ObservedObject
  • Add Combine Lifecycle logging during development
  • Check for retained cycles with weak self in sink closures
  • Use Instruments > Allocations to track AnyCancellable lifecycle
  • Consider using async/await for new code instead of Combine