0

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.

8
  • Just make it a computed property? var currentProgressInfo: String { transactionObserver.currentProgressInfo }. Am I missing something? Commented Mar 29 at 5:42
  • Seriously? Will it update the SwiftUI View? Wow... I will check...;) Commented Mar 29 at 5:43
  • Also the viewModel should be a @State. Commented Mar 29 at 5:44
  • Note, @Binding is only meaningful in a View, not in a class, eg class InAppPurchaseViewModel. Commented Mar 29 at 6:05
  • 1
    See Apple docs Managing model data in your app It says "Observation also supports tracking of computed properties when the computed property makes use of an observable property". Commented Mar 29 at 6:13

1 Answer 1

1

@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.

Sign up to request clarification or add additional context in comments.

5 Comments

So, you mean shared property of TransactionObserver class? It is 10-years code and I am trying to move the app into SwiftUI;) So simply TransactionObserver and InAppPurchaseViewModel should be @MainActor. I need to read more about that. Is that link correct source?
Tell me one more thing. Why it should be @MainActor?
@BartłomiejSemańczyk Marking the whole TransactionObserver as @MainActor wouldn’t work (at least not in Swift 6. You might be able to get away with it in Swift 5. I’m not sure) because the delegate methods need to be non-isolated. @MainActor essentially is a synchronisation mechanism. You make sure that every access to shared happens serially and not concurrently.
Yes, I understand, I thought about that: "What if it will be called the same time from two different places?" Then yes, it might be called concurrently... which leads to unexpected behaviour. So isolating TransactionObserver to @MainActor resolve that possible issue?
@BartłomiejSemańczyk yes. The main actor will make sure that one access happens after the other.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.