BehaviorRelay does not have one universal replacement in Combine. The right choice depends on who owns the state and who is allowed to change it.
Use @Published when the value belongs to one owning type and the rest of the app should only observe it.
That is the right default for most UI state:
- selected tab
- loading flag
- button enabled state
- search text
- current filter
One object owns the value, updates it internally, and exposes read-only observation to everyone else.
Use CurrentValueSubject when the subject itself is part of the design.
That usually means one of two things:
- multiple collaborators need to push values into the same stream
- you are bridging imperative events into
Combineand need an explicit input surface
The common migration mistake is replacing every BehaviorRelay with CurrentValueSubject just because both replay the latest value. That preserves replay, but often breaks ownership. A value that used to be controlled by one type suddenly becomes writable from anywhere that can reach the subject.
The practical default is simple.
- If it is owned state, start with
@Published. - If you genuinely need a subject, use
CurrentValueSubject.
A good migration does not just preserve the latest value. It preserves who is in charge of that value.
Simple Example
import Combine
final class InboxViewModel: ObservableObject {
@Published private(set) var selectedFilter = "all"
let searchInput = CurrentValueSubject<String, Never>("")
func selectUnread() {
selectedFilter = "unread"
}
}
selectedFilter is owned state, so the view model changes it internally.
searchInput is an explicit input surface, so a subject makes sense there.