Browse Questions
Answer:
Combine subscriptions and NotificationCenter block observers cause leaks when the observation token is stored on self while the callback closure captures self strongly.
In Combine, storing a subscription in var cancellables = Set<AnyCancellable>() on self while the pipeline's .sink closure references self creates a circular reference: self → cancellables → AnyCancellable → sink closure → self.
In NotificationCenter, addObserver(forName:object:queue:using:) returns an opaque observer token. If self retains the token and the observer closure strongly captures self, neither is ever deallocated unless removeObserver is called.
Code Example:
import Combine
import Foundation
class SearchViewModel {
@Published var query: String = ""
private var cancellables = Set<AnyCancellable>()
private var observerToken: NSObjectProtocol?
init() {
// ✅ Combine: Break cycle using [weak self] in sink
$query
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.sink { [weak self] searchQuery in
guard let self else { return }
self.performSearch(query: searchQuery)
}
.store(in: &cancellables)
// ✅ NotificationCenter: Break cycle using [weak self] & cleanup token
observerToken = NotificationCenter.default.addObserver(
forName: UIApplication.didEnterBackgroundNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.saveState()
}
}
deinit {
if let token = observerToken {
NotificationCenter.default.removeObserver(token)
}
}
private func performSearch(query: String) {}
private func saveState() {}
}
Key Points:
- Modern Swift apps should use Combine or
NotificationCenter.default.notifications(named:)async sequences with[weak self] AnyCancellableautomatically cancels its upstream publisher when deallocated, provided there is no retain cycle preventing its deallocation