iosswiftuibinding

How to Bind to property within @Observable from within @Observable using SwiftUI?


I am not sure if it is a good concept, but let's start from it here.

I have a simple View:

struct InAppPurchaseView: View {
    private let viewModel = InAppPurchaseViewModel()
    var body: some View {
        VStack {
            if !viewModel.currentProgressInfo.isEmpty {
                Text(viewModel.currentProgressInfo) // here it relies on the value from viewModel and should update every time when it changes
            }
        }
    }
}

@Observable
class InAppPurchaseViewModel {
    private let transactionObserver = TransactionObserver.shared
    @Binding var currentProgressInfo: String
    // here is the question❓ 
    // How to bind here property from within transactionObserver?
}

@Observable
class TransactionObserver: NSObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
    static let shared = TransactionObserver()
    var currentProgressInfo = ""

    // here is the code that updates currentProgressInfo depending on the needs.
}

Example of usage?

In the other view, suppose I have (totally abstract) two instances next to each other. Each has its own viewModel

struct StartView: View {
    var body: some View {
        VStack {
            InAppPurchaseView() // here it need to be updated
            InAppPurchaseView() // here it need to be updated THE SAME WAY.
        }
    }
}

Simply action taken in one of the above InAppPurchaseView should impact and update another one with the same effect.


Solution

  • @Observables track changes using the getters/setters of its properties (the @Observable macro inserts some code into the getters/setters to do this tracking). As long as a getter/setter is called during the evaluation of a view body, SwiftUI treats that as a dependency of the view, and will update the view when that property is set again.

    Your view body calls the getter of InAppPurchaseViewModel.currentProgressInfo, but you actually want TransactionObserver.currentProgressInfo to be a dependency of the view, so you can just have the getter of the former call the getter of the latter.

    // in InAppPurchaseViewModel...
    var currentProgressInfo: String {
        get { transactionObserver.currentProgressInfo }
        // if you also want InAppPurchaseView to be able to set the property...
        // set { transactionObserver.currentProgressInfo = newValue }
    }
    

    In any case, InAppPurchaseViewModel.currentProgressInfo should not be a stored property, because then you would end up with multiple sources of truth.


    Side note: viewModel in InAppPurchaseView should be a @State, and having a shared property is not concurrency safe. I would isolate shared to @MainActor, and isolate the whole InAppPurchaseViewModel class to @MainActor.