I have a button like this
Button(action: {
// do something
}, label: {
Text("The price is \(price)")
})
The first problem is that the label part of the button expects a View.
Also, the price is retrieved asynchronously from the app store using something like
func priceFor(_ productID: String,
onConclusion:GetPriceHanler?) {
inapp?.fetchProduct(productIdentifiers: [productID],
handler: { (result) in
switch result {
case .success(let products):
let products = products
guard let product = products.first
else {
onConclusion?("")
return
}
onConclusion?("\(product.price)")
case .failure(let error):
print(error.localizedDescription)
onConclusion?("")
}
})
}
So, this function runs a closure when it is done.
I have thought about creating another function that returns a view to call this one... something like
@ViewBuilder
func priceViewFor(_ productID: String) -> some View { }
I am OK if the function returns the price or empty if invalid, but if I do that, I will end inside a closure and from there I cannot return a view.
Something like
@ViewBuilder
func priceViewFor(_ productID: String) -> some View {
myAppStore.priceFor(productID) { price in
return Text(price) ?????????????
}
How in the name of bits do I do that?
The basic idea is to have a @State
that represents the price whether or not it has been fetched. Check this state in the label:
view builder, and if it is not fetched, show something else.
A simple design is to use an optional:
@State var price: String?
Button {
// do something
} label: {
// check if the price has been fetched
if let price {
Text("The price is \(price)")
} else {
ProgressView("Loading Price...")
}
}
.disabled(price == nil) // you might want to disable the button before the price has been fetched
.onAppear {
// set the state in the completion handler
myAppStore.priceFor(productID) { price in
self.price = price
}
}
Seeing how the price-fetching could fail, I would suggest using a Result
instead:
@State var price: Result<String, Error>?
switch price {
case .none:
ProgressView("Loading Price...")
case .failure(let error):
Label("Failed! (\(error))", systemImage: "xmark")
case .success(let price):
Text("The price is \(price)")
}
This would mean you would change priceFor
so that it passes a Result<String, Error>
to the onConclusion
closure.
Also, consider rewriting with the Swift Concurrency APIs. The usage would then look like:
.task {
do {
price = .success(try await myAppStore.priceFor(productID))
} catch {
price = .failure(error)
}
}