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
sinkorassignclosure is never called after first emissionAnyCancellablestored 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
AnyCancellablestored 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
.sinkwithout storing the returnedAnyCancellable - SwiftUI view recreation causing subscription loss
Step-by-Step Fix
- 1.Store cancellables in a Set property:
- 2.```swift
- 3.class ViewModel: ObservableObject {
- 4.@Published var items: [Item] = []
- 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.Use property wrapper for automatic cancellation:
- 2.```swift
- 3.@propertyWrapper
- 4.struct AutoCancellable {
- 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.Debug cancelled subscriptions:
- 2.```swift
- 3.extension Publisher {
- 4.func logLifecycle() -> Publishers.HandleEvents<Self> {
- 5.self.handleEvents(
- 6.receiveSubscription: { sub in
- 7.print("Subscribed: \(sub)")
- 8.},
- 9.receiveCancel: {
- 10.print("Cancelled at: \(Thread.callStackSymbols.prefix(5))")
- 11.}
- 12.)
- 13.}
- 14.}
// Usage apiClient.fetchData() .logLifecycle() .sink { ... } .store(in: &cancellables) ```
- 1.Handle SwiftUI view lifecycle correctly:
- 2.```swift
- 3.struct ContentView: View {
- 4.@StateObject private var viewModel = ViewModel()
- 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
@StateObjectfor ViewModels in SwiftUI, not@ObservedObject - Add Combine Lifecycle logging during development
- Check for retained cycles with
weak selfin sink closures - Use Instruments > Allocations to track AnyCancellable lifecycle
- Consider using async/await for new code instead of Combine