Duck
Duck

Reputation: 36003

How to update interface elements from asynchronous stuff from a singleton

I am developing my first app in SwiftUI and my brain have not wrapped around certain things yet.

I have to display prices, titles and descriptions of inapp purchases on the interface.

I have a singleton model like this, that loads as the app starts.

class Packages:ObservableObject {
  
enum Package:String, CaseIterable {
  typealias RawValue = String
    
  case package1 = "com.example.package1"
  case package2 = "com.example.package2"
  case package3 = "com.example.package3"
}
  
struct PurchasedItem {
  var productID:String?
  var localizedTitle:String = ""
  var localizedDescription:String = ""
  var localizedPrice:String = ""
}
  
static let sharedInstance = Packages()
  
@Published var purchasedItems:[PurchasedItem] = []

func getItem(_ productID:String) -> PurchasedItem? {
  return status.filter( {$0.productID == productID } ).first
}  

func getItemsAnync() {
  // this will fill `purchasedItems` asynchronously
}

purchasedItems array will be filled with PurchasedItems, asynchronous, as the values of price, title and description come from the App Store.

Meanwhile, at another part of the interface, I am displaying buttons on a view, like this:

var  buttonPackage1String:String {

  let item = Packages.sharedInstance.getItem(Packages.Package.package1.rawValue)!

  let string = """
    \(umItem.localizedTitle) ( \(umItem.localizedPrice) ) \
    \(umItem.localizedDescription) )
    """

  return string
}

// inside var Body

    Button(action: {
      // purchase selected package
    }) {
      Text(buttonPackage1String)
        .padding()
        .background(Color.green)
        .foregroundColor(.white)
        .padding()
  }

See my problem?

buttonPackage1String is built with title, description and price from an array called purchasedItems that is stored inside a singleton and that array is filled asynchronously, being initially empty.

So, at the time the view is displayed all values may be not retrieved yet but will, eventually.

Is there a way for the button is updated after the values are retrieved?

Upvotes: 1

Views: 87

Answers (2)

Quang Hà
Quang Hà

Reputation: 4746

Below is a small pseudo code, I'm not sure it's suitable for your project but might give you some hint.

Packages.sharedInstance
  .$purchasedItems //add $ to get the `Combine` Subject of variable
  .compactMap { $0.firstWhere { $0.id == Packages.Package.package1.rawValue } } //Find the correct item and skip nil value
  .map { "\($0.localizedTitle) ( \($0.localizedPrice) ) \ \($0.localizedDescription) )" }
  .receive(on: RunLoop.main) // UI updates
  .assign(to: \.buttonTitleString, on: self) //buttonTitleString is a @Published variable of your View model
  .store(in: &cancellables) //Your cancellable collector

Combine framework knowledge is a must to start with SwiftUI.

Upvotes: 1

Asperi
Asperi

Reputation: 258345

Calculable properties are not observed by SwiftUI. We have to introduce explicit state for this and update it once dependent dynamic data changed.

Something like,

@ObservedObject var packages: Packages

...

@State private var title: String = ""

...

    Button(action: {
      // purchase selected package
    }) {
      Text(title)
    }
    .onChange(of: packages.purchasedItems) { _ in
       self.title = buttonPackage1String          // << here !!
    }

Upvotes: 1

Related Questions