How to create #Preview with async code in swiftUI?
Asked Answered
C

2

7

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.

Chlorous answered 21/10, 2023 at 12:58 Comment(0)
S
8

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.

Seato answered 21/10, 2023 at 13:32 Comment(2)
Hello Yrb and thanks a lot. Your solution works as required. Have a nice evening. LiborChlorous
Thank you so much! Was struggling with providing a SwiftData model directly to a view that would normally be provided by another view that had used @Query. Since the querying is Async, it was much harder than I thought it would be. This fixed it.Cooks
C
3

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...")
        }
    }
}
Confederate answered 1/2, 2024 at 12:49 Comment(2)
Thank you Cemen, I will have a look at your solution. Libor.Chlorous
elegant solution, thanksClipboard

© 2022 - 2025 — McMap. All rights reserved.