A lot of RxSwift migrations underestimate Driver. It looked like a convenient type for UI, but it was really a bundle of guarantees.
Driver meant:
- main-thread delivery
- no failure in the UI layer
- shared side effects
- replay of the latest value to new subscribers
That combination is why it felt safe in view models and screen bindings. Teams relied on those guarantees even when they never wrote them down.
Combine does not give you that contract in one built-in type.
receive(on:)only handles delivery.- It does not remove failures.
- It does not replay the current value.
- It does not automatically share upstream work.
- It does not stop multiple subscribers from triggering duplicate side effects.
This is why naive migrations break behavior.
- A screen that used to show the latest state immediately may now wait for the next emission.
- A publisher that used to do one network-backed transformation may now do it twice because two subscribers attached.
- A stream that could never fail into the UI may now need explicit error handling.
The practical migration rule is this: stop asking what replaces Driver, and start asking which Driver guarantees this stream actually depended on.
Sometimes the answer is:
- only main-thread delivery
- replay
- sharing
- all of it
A successful migration is not when Driver disappears. It is when every guarantee it used to provide is either preserved intentionally or removed intentionally.
Simple Example
import Combine
final class ProfileViewModel {
private let state = CurrentValueSubject<String, Never>("Loading")
var titlePublisher: AnyPublisher<String, Never> {
state
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
func didLoad(name: String) {
state.send(name)
}
}
This sample keeps three guarantees visible:
Nevermeans the UI-facing stream does not failCurrentValueSubjectreplays the latest valuereceive(on:)keeps delivery on the main thread
If the original Driver also depended on shared upstream work, that part still needs to be preserved separately.