assign looks like the obvious replacement for Rx bind. In UI code, it often is not the safest one.
The reason is simple. UI binding is rarely just about moving a value from one stream into one property. It is also about:
- scheduler choice
- object lifetime
- reuse
- making sure updates do not keep targeting UI that should already be gone
That matters most in:
- controllers
- cells
- reusable views
A stream may outlive the visible UI. A reused cell may still receive updates meant for its previous content. A label or button may disappear while the upstream publisher keeps emitting.
This is where receive(on:) + sink is often better. It makes the important parts explicit.
- You decide where delivery happens.
- You choose weak capture when the target should not be retained.
- You can see subscription lifetime clearly instead of hiding it behind a very short binding expression.
assign is still valid when the target is stable and strongly owned for the full subscription lifetime. But that should be a deliberate decision, not the default migration reflex.
The key point is that Rx bind carried ergonomic safety that developers often took for granted. In Combine, you usually have to rebuild that safety manually.
If lifecycle matters, explicit beats short. For UI binding, that is usually the right tradeoff.
Simple Example
import Combine
import UIKit
final class ProfileViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
private let nameLabel = UILabel()
func bind(namePublisher: AnyPublisher<String, Never>) {
namePublisher
.receive(on: RunLoop.main)
.sink { [weak self] name in
self?.nameLabel.text = name
}
.store(in: &cancellables)
}
}
This keeps the scheduler explicit, uses weak capture, and makes the subscription lifetime visible.
That is usually safer for UI than hiding everything behind one short assign.