Combine and SwiftUI onReceive
8th
September 2025
I recently wanted to build a custom publisher using Apple’s Swift Combine framework. It was hard work until I saw an answer by rob mayoff showing how to wrap an existing publisher. Obvious, but in my case, only in hindsight.
As a side effect of my work I was able to work out what SwiftUI does when you use View.onReceive(_:perform:). Below are the state diagrams for the view appearing, disappearing, and a button press:
Appear
> Subject receive(subscriber:)
> Subscriber receive(subscription:)
- Subscription request(_:) demand: unlimited
- Subscription request(_:) demand: unlimited
< Subscriber receive(subscription:)
< Subject receive(subscriber:)
- View onAppear
Disappear
> Subscription cancel()
- Subscriber deinit
< Subscription cancel()
- Subscription deinit
- View onDisappear
Send (via button press)
> Button perform(:)
> Subject send(_:) value: «value»
- Subscriber receive(_:) input: «value»
< Subject send(_:)
< Button perform(:)
- View onReceive(:) output: «value»
The code
Here’s the outline of the code I used to completely wrap a Publisher, a Subject, and for added points, CurrentValueSubject. It doesn’t strictly do anything but you can use it as a base to add any extras you might need. It also does a reasonable job of showing you how publishers are structured, if you look at it for long enough.
import Combine
final class Tracker<Output, Failure, Target>: Publisher where Target: Publisher, Output == Target.Output, Failure == Target.Failure {
private let target: Target
init(_ target: Target) {
self.target = target
}
func receive<Downstream: Subscriber>(subscriber: Downstream) where Failure == Downstream.Failure, Output == Downstream.Input {
target.subscribe(TrackedSubscriber(subscriber))
}
deinit { }
}
extension Tracker: Subject where Target: Subject {
func send(_ value: Output) {
target.send(value)
}
func send(completion: Subscribers.Completion<Failure>) {
target.send(completion: completion)
}
func send(subscription: Subscription) {
target.send(subscription: subscription)
}
}
extension Tracker where Target == CurrentValueSubject<Output, Failure> {
var value: Output {
get { target.value }
set { send(newValue) }
}
}
final class TrackedSubscriber<Input, Failure, Target>: Subscriber where Target: Subscriber, Input == Target.Input, Failure == Target.Failure {
private let target: Target
init(_ target: Target) {
self.target = target
}
func receive(subscription: any Subscription) {
target.receive(subscription: TrackedSubscription(subscription))
}
func receive(_ input: Input) -> Subscribers.Demand {
return target.receive(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
target.receive(completion: completion)
}
deinit { }
}
final class TrackedSubscription: Subscription {
private let target: Subscription
init(_ target: Subscription) {
self.target = target
}
func request(_ demand: Subscribers.Demand) {
target.request(demand)
}
func cancel() {
target.cancel()
}
deinit { }
}