A simple calculator using @Observable, withObservationTracking and AsyncStream
Following on from my previous post on this topic I found a forum post with someone struggling to build a calculator using @Observable
and thought maybe my technique of wrapping withObservationTracking
in AsyncStream
might help and came up with the code below which appears to work. Obviously this code comes with a health warning that using a func calculate()
, computed var sum
or use didSet
on the inputs would be a much more sensible implementation for a simple calculator that isn’t doing any long running or intensive calculations.
import SwiftUI
import AsyncAlgorithms
@MainActor
struct Calculator {
@Observable
class Model {
var a: Int?
var b: Int?
var sum: Int = 0
}
let model = Model()
static var shared = Calculator()
init() {
Task { [model] in
let aDidChange = AsyncStream {
await withCheckedContinuation { continuation in
let _ = withObservationTracking {
model.a
} onChange: {
continuation.resume()
}
}
return model.a
}
.compacted().removeDuplicates()
let bDidChange = AsyncStream {
await withCheckedContinuation { continuation in
let _ = withObservationTracking {
model.b
} onChange: {
continuation.resume()
}
}
return model.b
}
.compacted().removeDuplicates()
for await x in combineLatest(aDidChange, bDidChange).map(+) {
model.sum = x
}
}
}
}
struct ContentView: View {
let calculator = Calculator.shared
var body: some View {
Form {
Section("ObservedObject") {
let bindable = Bindable(calculator.model)
TextField("a", value: bindable.a, format: .number)
TextField("b", value: bindable.b, format: .number)
Text(calculator.model.sum, format: .number)
}
}
}
}
The idea to use .map(+)
was taken from the original forum post in their Combine pipeline however here it errors with `Converting non-sendable function value to ‘@Sendable(…` so am not sure yet how to fix that but will look into it.
In case you are wondering why I used Task in a singleton instead of .task
it’s just because I’ve been working on app level async services lately and there currently is no similar modifier for those. If you would like the calculation to be cancelled when the UI disappears then .task
is probably a better option.