I spent quite a bit of time trying to figure out how to avoid the following Swift 6 error in the function below (which aims to receive an AsyncStream of property value changes) but failed so far:
Capture of 'observe()' with non-sendable type '() -> ()' in a `@Sendable` closure.
The call to observe() obviously doesn't race, but how can I get Swift to take this into account? Note: The sendable closure is the onChange parameter.
@MainActor
func withContinuousMainActorObservation<T>(
of property: @autoclosure @escaping @MainActor () -> T
) -> AsyncStream<T> {
AsyncStream { continuation in
@MainActor func observe() {
let value = withObservationTracking {
property()
} onChange: {
Task { @MainActor in
observe()
}
}
continuation.yield(value)
}
observe()
}
}
I've also written a different, more complex implementation but all have the same issue. Thanks for any hints.
Unfortunately, I'm not allowed to annotate a local function with @Sendable and @MainActor. This leads to the following error:
Main actor-isolated synchronous local function 'observe()' cannot be marked as '@Sendable'
Which is odd but also unnecessary if local functions isolated to global actors are to be implicitly sendable indeed. I agree it would be nice to have a clarification here.
This is actually exactly the same goal and it was interesting to see that you both came to the same conclusions. One thing to keep in mind about the withObservationTracking API is that the onChange closure is called on willSet of the observed property not didSet.
The current, compiler error-free implementation looks like this after adopting some ideas:
@MainActor
public func withContinuousMainActorObservation<T>(
of property: @autoclosure @escaping () -> sending T
) -> AsyncStream<T> {
nonisolated(unsafe) let property = property
return AsyncStream { continuation in
let isTerminated = OSAllocatedUnfairLock(initialState: false)
continuation.onTermination = { _ in
isTerminated.withLock { $0 = true }
}
@Sendable func observe() {
let value = withObservationTracking {
property()
} onChange: {
if !isTerminated.withLock(\.self) {
Task { @MainActor in
observe()
}
}
}
if !isTerminated.withLock(\.self) {
continuation.yield(value)
}
}
observe()
}
}
Thanks again for the valuable answer. I hope the compiler will be further improved to be able to remove the unsafe workaround. Should I also report a bug given yours is the same?