The Hidden Power of connect() and autoconnect() in Combine Swift

The Hidden Power of connect() and autoconnect() in Combine Swift

Source: Dev.to

What is a Connectable Publisher? ## The connect() Method ## Basic Example ## Key Points about connect() ## When to Use connect() ## Practical Example ## The autoconnect() Method ## Basic Example ## Key Points about autoconnect() ## When to Use autoconnect() ## Timer Example ## connect() vs autoconnect(): The Decision Matrix ## Common Pattern: share() with multicast ## Memory Management Tips ## Quick Reference ## Conclusion Have you ever subscribed to a Combine publisher and wondered why nothing happened? You set up your pipeline perfectly, added your subscriber, but no values flow through. Meanwhile, your app just sits there, seemingly ignoring your carefully crafted reactive code. Here's the secret: some publishers in Combine don't start working just because you subscribed to them. They're waiting for you to give them permission to start, and that's where connect() and autoconnect() come in. A connectable publisher is a special type of publisher that waits for an explicit signal before it starts doing work. The most common example is the share() operator combined with makeConnectable(), or when using multicast(). Think of it like a water faucet. Normally, when you turn the handle (subscribe), water flows immediately. But with a connectable publisher, turning the handle just prepares the system. You need to push a separate button (connect) to actually start the flow. The connect() method manually starts a connectable publisher. This gives you precise control over when the publisher begins emitting values. Use connect() when you need to: The autoconnect() method eliminates manual connection by automatically calling connect() when the first subscriber subscribes. This is the "set it and forget it" approach. Use autoconnect() when: When you want to share a single subscription among multiple subscribers, you often combine these concepts: Remember to store your connections and subscriptions: Use connect() when you need control: Use autoconnect() when you want simplicity: Understanding connect() and autoconnect() gives you powerful control over when and how your Combine pipelines execute. The key is recognizing that not all publishers start working immediately, and these tools give you the flexibility to manage that behavior effectively. Templates let you quickly answer FAQs or store snippets for re-use. Use connect() when you need control: Use autoconnect() when you want simplicity: Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: import Combine let subject = PassthroughSubject<Int, Never>() // Create a connectable publisher let connectable = subject .print("Debug") .makeConnectable() // Subscribe first let subscription1 = connectable .sink { value in print("Subscriber 1 received: \(value)") } let subscription2 = connectable .sink { value in print("Subscriber 2 received: \(value)") } // Nothing happens yet, even though we have subscribers // Now manually connect let connection = connectable.connect() // Now values will flow to all subscribers subject.send(1) subject.send(2) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: import Combine let subject = PassthroughSubject<Int, Never>() // Create a connectable publisher let connectable = subject .print("Debug") .makeConnectable() // Subscribe first let subscription1 = connectable .sink { value in print("Subscriber 1 received: \(value)") } let subscription2 = connectable .sink { value in print("Subscriber 2 received: \(value)") } // Nothing happens yet, even though we have subscribers // Now manually connect let connection = connectable.connect() // Now values will flow to all subscribers subject.send(1) subject.send(2) CODE_BLOCK: import Combine let subject = PassthroughSubject<Int, Never>() // Create a connectable publisher let connectable = subject .print("Debug") .makeConnectable() // Subscribe first let subscription1 = connectable .sink { value in print("Subscriber 1 received: \(value)") } let subscription2 = connectable .sink { value in print("Subscriber 2 received: \(value)") } // Nothing happens yet, even though we have subscribers // Now manually connect let connection = connectable.connect() // Now values will flow to all subscribers subject.send(1) subject.send(2) CODE_BLOCK: // Expensive network request let publisher = URLSession.shared .dataTaskPublisher(for: url) .map(\.data) .decode(type: User.self, decoder: JSONDecoder()) .share() .makeConnectable() // Set up multiple subscribers let sub1 = publisher.sink( receiveCompletion: { _ in }, receiveValue: { user in updateUI(user) } ) let sub2 = publisher.sink( receiveCompletion: { _ in }, receiveValue: { user in saveToCache(user) } ) // Only make one network request for both subscribers let connection = publisher.connect() Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // Expensive network request let publisher = URLSession.shared .dataTaskPublisher(for: url) .map(\.data) .decode(type: User.self, decoder: JSONDecoder()) .share() .makeConnectable() // Set up multiple subscribers let sub1 = publisher.sink( receiveCompletion: { _ in }, receiveValue: { user in updateUI(user) } ) let sub2 = publisher.sink( receiveCompletion: { _ in }, receiveValue: { user in saveToCache(user) } ) // Only make one network request for both subscribers let connection = publisher.connect() CODE_BLOCK: // Expensive network request let publisher = URLSession.shared .dataTaskPublisher(for: url) .map(\.data) .decode(type: User.self, decoder: JSONDecoder()) .share() .makeConnectable() // Set up multiple subscribers let sub1 = publisher.sink( receiveCompletion: { _ in }, receiveValue: { user in updateUI(user) } ) let sub2 = publisher.sink( receiveCompletion: { _ in }, receiveValue: { user in saveToCache(user) } ) // Only make one network request for both subscribers let connection = publisher.connect() CODE_BLOCK: let timer = Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() let subscription = timer.sink { date in print("Timer fired at: \(date)") } // Timer starts immediately when subscribed Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: let timer = Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() let subscription = timer.sink { date in print("Timer fired at: \(date)") } // Timer starts immediately when subscribed CODE_BLOCK: let timer = Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() let subscription = timer.sink { date in print("Timer fired at: \(date)") } // Timer starts immediately when subscribed CODE_BLOCK: class ViewModel: ObservableObject { @Published var seconds = 0 private var cancellables = Set<AnyCancellable>() func startTimer() { Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in self?.seconds += 1 } .store(in: &cancellables) } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: class ViewModel: ObservableObject { @Published var seconds = 0 private var cancellables = Set<AnyCancellable>() func startTimer() { Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in self?.seconds += 1 } .store(in: &cancellables) } } CODE_BLOCK: class ViewModel: ObservableObject { @Published var seconds = 0 private var cancellables = Set<AnyCancellable>() func startTimer() { Timer.publish(every: 1.0, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in self?.seconds += 1 } .store(in: &cancellables) } } CODE_BLOCK: let shared = expensivePublisher .share() // Creates a connectable publisher .autoconnect() // Starts automatically // Both subscribers share the same upstream work let sub1 = shared.sink { print("A: \($0)") } let sub2 = shared.sink { print("B: \($0)") } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: let shared = expensivePublisher .share() // Creates a connectable publisher .autoconnect() // Starts automatically // Both subscribers share the same upstream work let sub1 = shared.sink { print("A: \($0)") } let sub2 = shared.sink { print("B: \($0)") } CODE_BLOCK: let shared = expensivePublisher .share() // Creates a connectable publisher .autoconnect() // Starts automatically // Both subscribers share the same upstream work let sub1 = shared.sink { print("A: \($0)") } let sub2 = shared.sink { print("B: \($0)") } CODE_BLOCK: class DataManager { private var connection: Cancellable? private var subscriptions = Set<AnyCancellable>() func setupConnectable() { let connectable = publisher.makeConnectable() connectable .sink { value in print(value) } .store(in: &subscriptions) // Store the connection connection = connectable.connect() } deinit { connection?.cancel() } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: class DataManager { private var connection: Cancellable? private var subscriptions = Set<AnyCancellable>() func setupConnectable() { let connectable = publisher.makeConnectable() connectable .sink { value in print(value) } .store(in: &subscriptions) // Store the connection connection = connectable.connect() } deinit { connection?.cancel() } } CODE_BLOCK: class DataManager { private var connection: Cancellable? private var subscriptions = Set<AnyCancellable>() func setupConnectable() { let connectable = publisher.makeConnectable() connectable .sink { value in print(value) } .store(in: &subscriptions) // Store the connection connection = connectable.connect() } deinit { connection?.cancel() } } CODE_BLOCK: Multiple subscribers before starting Coordination with app state Explicit timing requirements Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Multiple subscribers before starting Coordination with app state Explicit timing requirements CODE_BLOCK: Multiple subscribers before starting Coordination with app state Explicit timing requirements CODE_BLOCK: Immediate start on subscription Timers and periodic publishers Single subscriber scenarios Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Immediate start on subscription Timers and periodic publishers Single subscriber scenarios CODE_BLOCK: Immediate start on subscription Timers and periodic publishers Single subscriber scenarios - Manual Control: You decide exactly when the publisher starts working - Multiple Subscribers: All subscribers receive values once connected - Returns Cancellable: The connection itself can be cancelled - One-Time Action: Calling connect() once starts the flow for all subscribers - Wait until multiple subscribers are set up before starting work - Coordinate the start of data flow with other application events - Perform expensive operations only after you're ready - Synchronize multiple pipelines - Automatic Start: Connects as soon as the first subscriber appears - Convenience: No need to manage the connection manually - Common Pattern: Used frequently with Timer publishers - Transparent: Feels like a regular publisher to the subscriber - You want the publisher to start immediately upon subscription - You're working with timers or periodic events - You don't need to coordinate multiple subscribers before starting - Simplicity is more important than precise timing control - Multiple subscribers before starting - Coordination with app state - Explicit timing requirements - Immediate start on subscription - Timers and periodic publishers - Single subscriber scenarios - Location London - Work Work on mobile application as Tech expert - Joined Jun 11, 2025