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.
What is a Connectable Publisher?
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. Normal…
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.
What is a Connectable Publisher?
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
The connect() method manually starts a connectable publisher. This gives you precise control over when the publisher begins emitting values.
Basic Example
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)
Key Points about connect()
- 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
When to Use connect()
Use connect() when you need to:
- 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
Practical Example
// 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()
The autoconnect() Method
The autoconnect() method eliminates manual connection by automatically calling connect() when the first subscriber subscribes. This is the "set it and forget it" approach.
Basic Example
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
Key Points about autoconnect()
- 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
When to Use autoconnect()
Use autoconnect() when:
- 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
Timer Example
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)
}
}
connect() vs autoconnect(): The Decision Matrix
| Scenario | Use connect() | Use autoconnect() |
|---|---|---|
| Need to wait for multiple subscribers | Yes | No |
| Want immediate start on subscription | No | Yes |
| Timer or periodic events | Rarely | Usually |
| Expensive shared operation | Yes | Maybe |
| Simple use case | No | Yes |
| Need coordination with other events | Yes | No |
Common Pattern: share() with multicast
When you want to share a single subscription among multiple subscribers, you often combine these concepts:
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)") }
Memory Management Tips
Remember to store your connections and subscriptions:
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()
}
}
Quick Reference
Use connect() when you need control:
- Multiple subscribers before starting
- Coordination with app state
- Explicit timing requirements
Use autoconnect() when you want simplicity:
- Immediate start on subscription
- Timers and periodic publishers
- Single subscriber scenarios
Conclusion
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.