A simple calculator using @Observable, withObservationTracking and AsyncStream

Published by malhal on

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.

نيك قحاب arabianmotion.com سكس ليلةالدخلة hot indian girl nude fuckvidstube.com 3gp porn movies 全裸婚活パーティー javsearch.mobi サカリ 葉月桃 xxx heroine movsmo.net telugu heroines sex سكس ميا porn-planet.org سكسى نيك
hot natalie martinez tubemania.org xvideks افلام كلاسيك مترجمه esarabe.com ادوج فينش watch mygf pornmd.pro gujarati indian sex indian high profile sex manytubeporn.mobi downloadhub.link سكس فى الغابه realarabporn.com سكس المصيف
افلام جنس قصيرة arabianreps.com جسمها نار romance x indianpornvideos.me bada dudha 無修正 マジックミラー javmovie.pro 鬱勃起 افضل مواقع السكس المصري pornwap.tv سكس بيوت الدعاره سكس مصريhd arab-porn.org كس متحرك