The Hidden Power of connect() and autoconnect() in Combine Swift
Source: Dev.to
Introduction
When you subscribe to a Combine publisher, you might expect values to start flowing immediately. However, some publishers wait for an explicit signal before they begin work. Connectable publishers give you that control, and connect() and autoconnect() are the two ways to start them.
connect() – Manual Control
A connectable publisher does nothing until you call its connect() method. This lets you:
- Set up multiple subscribers before any values are emitted.
- Coordinate the start of data flow with other app events.
- Avoid expensive work until you’re ready.
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
// Manually start the publisher
let connection = connectable.connect()
// Now values flow to all subscribers
subject.send(1)
subject.send(2)
Key points
- Manual start – you decide exactly when the publisher begins.
- Multiple subscribers – all receive values once connected.
- Cancellable – the
connectioncan be cancelled to stop the flow.
When to use connect()
- You need to wait until several subscribers are ready.
- You want to synchronize the start with other events.
- The upstream work is expensive and should run only once.
Real‑world scenario: shared network request
let url = URL(string: "https://example.com/user")!
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 one network request is performed
let connection = publisher.connect()
autoconnect() – Automatic Start
autoconnect() removes the need for a manual connect() call. The publisher automatically connects as soon as the first subscriber appears.
Example
let timer = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
let subscription = timer.sink { date in
print("Timer fired at: \(date)")
}
Key points
- Immediate start – the publisher begins as soon as it gets its first subscriber.
- Convenient – no need to store a separate connection.
- Common with timers – ideal for periodic events.
When to use autoconnect()
- You want the publisher to start right away upon subscription.
- You’re dealing with timers or other periodic sources.
- Precise coordination of multiple subscribers isn’t required.
ViewModel 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)
}
}
Choosing Between connect() and autoconnect()
| Requirement | Use connect() | Use autoconnect() |
|---|---|---|
| Need to wait for multiple subscribers | ✅ | ❌ |
| Immediate start on subscription | ❌ | ✅ |
| Timer or periodic events | ❌ (possible) | ✅ |
| Expensive shared operation | ✅ | ✅ (if immediate start is fine) |
| Simple use case | ❌ | ✅ |
| Coordination with other events | ✅ | ❌ |
Sharing a Single Subscription
Often you’ll combine share() (which creates a connectable publisher) with either connect() or autoconnect() to ensure multiple downstream subscribers share the same upstream work.
let shared = expensivePublisher
.share() // Makes the publisher connectable
.autoconnect() // Starts automatically (or use .connect() manually)
let sub1 = shared.sink { print("A: \($0)") }
let sub2 = shared.sink { print("B: \($0)") }
Storing Connections and Subscriptions
Remember to keep references to your connections and cancellables, otherwise they’ll be deallocated and the publisher will stop.
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 so it can be cancelled later
connection = connectable.connect()
}
deinit {
connection?.cancel()
}
}
Conclusion
connect() and autoconnect() give you precise control over when a Combine pipeline starts emitting values. Use connect() when you need explicit timing and coordination, and autoconnect() when you prefer simplicity and immediate start—especially with timers or other periodic publishers. Understanding these tools lets you manage resource usage and data flow more effectively in your Swift applications.