Skip to content

Instantly share code, notes, and snippets.

@niw
Created March 25, 2025 01:26
Show Gist options
  • Save niw/d6d14f6e4bb28538451b1734aee9df70 to your computer and use it in GitHub Desktop.
Save niw/d6d14f6e4bb28538451b1734aee9df70 to your computer and use it in GitHub Desktop.
Experimental `onChange(of:initial:perform:)`
import Foundation
import Observation
func onChange<Value>(
of value: @autoclosure () -> Value,
initial: Bool = false,
perform: (_ oldValue: Value, _ newValue: Value) -> Void,
_: isolated Actor = #isolation
) async throws {
let (stream, continuation) = AsyncThrowingStream.makeStream(of: Void.self)
var iterator = stream.makeAsyncIterator()
var oldValue: Value?
repeat {
let newValue = withObservationTracking {
value()
} onChange: {
continuation.yield()
}
if let _oldValue = oldValue {
perform(_oldValue, newValue)
} else {
oldValue = newValue
if initial {
// NOTE: This behavior same as `onChange(of:initial:perform:)` on SwiftUI `View`.
perform(newValue, newValue)
}
}
oldValue = newValue
if Task.isCancelled {
// This is only finishing point, by cancelling Task.
continuation.finish(throwing: CancellationError())
}
} while try await iterator.next() != nil
// Should not reach here because previous `try` throws with `CancellationError`
// is the only finishing point.
fatalError("Should not reach here.")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment