I would like to be able to create a #Preview that uses some values that are computed with async/await.
Below is a sample code with the first 2 previews working fine. The last one calling an async func fails.
import SwiftUI
import SwiftData
struct TestUIView: View {
var passedValue:String
var body: some View {
Text("passedValue: \(passedValue)")
}
}
#Preview {
let passedValue="My value"
return TestUIView(passedValue: passedValue)
}
#Preview {
let passedValue=MyClass.getValue()
return TestUIView(passedValue: passedValue)
}
#Preview {
var passedValue:String {
get async {
return await MyClass.getValueAsync()
}
}
return TestUIView(passedValue: passedValue)
}
class MyClass {
public class func getValue() -> String {
return "from getValue"
}
public class func getValueAsync() async -> String {
return "from getValueAsync"
}
}
The Preview error is: Compiling failed: 'async' property access in a function that does not support concurrency
In the real app, the data passed on is created in async mode and once available passed on to the next View. I want to be able to use the same function that creates this data in the Preview, rather then creating some dummy data only for the #Preview.
The way you are going to solve this Preview Macro problem is the same way you would solve other issues such as using @State variables: create a view struct within the macro.
#Preview("from getValueAsync") {
struct AsyncTestView: View {
@State var passedValue: String = ""
var body: some View {
TestUIView(passedValue: passedValue)
.task {
passedValue = await MyClass.getValueAsync()
}
}
}
return AsyncTestView()
}
You can also create it outside of your preview, but then archiving would not strip the code when creating a release. Another thing, If you are using multiple macros, it helps to name them.
A bit more generic approach, to avoid creating boilerplate supplementary structs every time.
#Preview {
AsyncModel { asyncValue in
TestUIView(passedValue: asyncValue)
} model: {
await MyClass.getValueAsync()
}
}
It's powered by small generic struct that also supports error handling.
struct AsyncModel<VisualContent: View, ModelData>: View {
// Standard view builder, accepting async-fetched data as a parameter
var viewBuilder: (ModelData) -> VisualContent
// data fetcher. Notice it can throw as well
var model: () async throws -> ModelData?
@State private var modelData: ModelData?
@State private var error: Error?
var body: some View {
safeView
.task {
do {
self.modelData = try await model()
} catch {
self.error = error
// print detailed error info to console
print(error)
}
}
}
@ViewBuilder
private var safeView: some View {
if let modelData {
viewBuilder(modelData)
}
// in case of error, its description rendered
// right on preview to make troubleshooting faster
else if let error {
Text(error.localizedDescription)
.foregroundStyle(Color.red)
}
// a stub for awaiting.
// Actually, we should return some non-empty view from here
// to make sure .task { } is triggered
else {
Text("Calculating async data...")
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With