How to await inside SwiftUI a #Preview?

In order to setup a preview, I need to create a Book; to do that, I need to await a function – however, one cannot await inside a Preview:

import SwiftUI

struct BookView: View {
    let book: Book
    
    var body: some View {
        VStack {
            Image(book.thumbnail, scale: 1.0,
                  label: Text(book.title))
        }
    }
}



#Preview {
    let url = URL(filePath: "/Users/dan/Documents/Curs confirmare RO.pdf")!
         // 👇 here, `createBook` should be awaited; but how?
  let book = createBook(for: url, of: CGSize(width: 254, height: 254), scale: 1.0)
    
    BookView(book: book)
}
Answered by Developer Tools Engineer in 827274022

The above is a great quick / simple solution. Here is another option, which may take a little more setup, but can provide a flexible solution if you regularly need to load data for use across your previews.

You can use a combination of the PreviewModifier and the @Previewable API that were introduced for use with #Preview last year to create any content you need for your previews.

The general approach here is to write a preview modifier that creates the content and later modifies any previews that have the modifier applied with that content. Then you can use @Previewable to retrieve that content in your preview and pass it to the view you are previewing as needed.

In the below example I will just create a simple model for use in a single preview, but you could expand the context being created in the preview modifier to create a preview data object that contained data for many previews and apply it where needed. You can also make as many preview modifier types as makes sense for the different kinds of data in your project and apply them to previews as needed.

To start, here is my simple model for this example:

struct Book {
    // Interesting properties here...
    
    static func createBook(for url: URL) async -> Book {
        // Await the content here...
        // Initialize the Book with actual content...
        return Book()
    }
}

First we set up what we need to pass the content through the environment:

extension EnvironmentValues {
    @Entry var previewBook: Book = {
        // This is the default value and will only be used if you forget to set your preview modifier
        // trait.
        fatalError("No value set for previewBook in the environment, did you forget your preview modifier trait?")
    }()
}

extension View {
    func previewBook(_ book: Book) -> some View {
        environment(\.previewBook, book)
    }
}

Then we can create the PreviewModifier that will load the data and modify the preview content with that data:

struct BookContent: PreviewModifier {
    // This async throws function is called to create the context for the preview modifer
    // that can then be applied to preview content.
    static func makeSharedContext() async throws -> Book {
        let url = URL(filePath: "/My/Test/Data.pdf")!
        return await Book.createBook(for: url)
    }

    // This is called to modify your preview content with the context created above whenever
    // you apply your preview modifier to a #Preview.
    func body(content: Content, context: Book) -> some View {
        content.previewBook(context)
    }
}

extension PreviewTrait where T == Preview.ViewTraits {
    // Create a convenience for applying the above preview modifier to a preview
    static var previewBook: Self {
        .modifier(BookContent())
    }
}

The last steps are to apply the BookContent preview modifier to any #Previews where the Book value is needed and to use @Previewable to read that value from the environment to pass in to the view being previewed:

struct BookView: View {
    let book: Book
    
    var body: some View {
        VStack {
            // Use your book values...
            Text("Book Data Here...")
                .padding()
        }
    }
}

#Preview(traits: .previewBook) {
    // @Previewable allows us to use normal SwiftUI attributes like @Environment, @State,
    // and @Binding in a preview body
    @Previewable @Environment(\.previewBook) var book

    BookView(book: book)
}

Hi,

This is an interesting problem. I see a few options, but I'd probably first reach for creating a book loading view, so something like:

#Preview {
    struct BookLoadingView: View {
        let bookURL: URL
        @State var book: Book? = nil
    
        var body: some View {
            if let book {
                BookView(book: book)
            } else {
                ProgressView()
                    .task {
                        book = await createBook(for: url, of: CGSize(width: 254, height: 254), scale: 1.0)
                    }
            }
        }
    }

    let url = URL(filePath: "/Users/dan/Documents/Curs confirmare RO.pdf")!
    BookLoadingView(bookURL: url)
}

Pardon any compiler errors, this was typed straight in to the browser, but hopefully this will get you close enough!

The above is a great quick / simple solution. Here is another option, which may take a little more setup, but can provide a flexible solution if you regularly need to load data for use across your previews.

You can use a combination of the PreviewModifier and the @Previewable API that were introduced for use with #Preview last year to create any content you need for your previews.

The general approach here is to write a preview modifier that creates the content and later modifies any previews that have the modifier applied with that content. Then you can use @Previewable to retrieve that content in your preview and pass it to the view you are previewing as needed.

In the below example I will just create a simple model for use in a single preview, but you could expand the context being created in the preview modifier to create a preview data object that contained data for many previews and apply it where needed. You can also make as many preview modifier types as makes sense for the different kinds of data in your project and apply them to previews as needed.

To start, here is my simple model for this example:

struct Book {
    // Interesting properties here...
    
    static func createBook(for url: URL) async -> Book {
        // Await the content here...
        // Initialize the Book with actual content...
        return Book()
    }
}

First we set up what we need to pass the content through the environment:

extension EnvironmentValues {
    @Entry var previewBook: Book = {
        // This is the default value and will only be used if you forget to set your preview modifier
        // trait.
        fatalError("No value set for previewBook in the environment, did you forget your preview modifier trait?")
    }()
}

extension View {
    func previewBook(_ book: Book) -> some View {
        environment(\.previewBook, book)
    }
}

Then we can create the PreviewModifier that will load the data and modify the preview content with that data:

struct BookContent: PreviewModifier {
    // This async throws function is called to create the context for the preview modifer
    // that can then be applied to preview content.
    static func makeSharedContext() async throws -> Book {
        let url = URL(filePath: "/My/Test/Data.pdf")!
        return await Book.createBook(for: url)
    }

    // This is called to modify your preview content with the context created above whenever
    // you apply your preview modifier to a #Preview.
    func body(content: Content, context: Book) -> some View {
        content.previewBook(context)
    }
}

extension PreviewTrait where T == Preview.ViewTraits {
    // Create a convenience for applying the above preview modifier to a preview
    static var previewBook: Self {
        .modifier(BookContent())
    }
}

The last steps are to apply the BookContent preview modifier to any #Previews where the Book value is needed and to use @Previewable to read that value from the environment to pass in to the view being previewed:

struct BookView: View {
    let book: Book
    
    var body: some View {
        VStack {
            // Use your book values...
            Text("Book Data Here...")
                .padding()
        }
    }
}

#Preview(traits: .previewBook) {
    // @Previewable allows us to use normal SwiftUI attributes like @Environment, @State,
    // and @Binding in a preview body
    @Previewable @Environment(\.previewBook) var book

    BookView(book: book)
}

@Developer Tools Engineer , you are giving a bad example because this will lead to a crash:

extension EnvironmentValues {
    @Entry var previewBook: Book = {
        // This is the default value and will only be used if you forget to set your preview modifier
        // trait.
        fatalError("No value set for previewBook in the environment, did you forget your preview modifier trait?")
    }()
}

@VAndrJ you are correct that the default value will cause a crash in the case where Book is not in the SwiftUI environment. However, if this environment value is only for use in your previews and you always set the preview trait when needed in those previews, it seems like having a crash with a helpful message to inform you that you forgot is desirable.

Having said that, wrapping this code and the related preview modifier / previews in #if DEBUG or some similar flag in your project to ensure that usage of it does not reach your production app would probably be a good idea. You could also remove the fatalError and put in some reasonable default value if that makes sense for your use case.

@Developer Tools Engineer it will cause a crash even when Book is in the SwiftUI environment. Because the @Environment value got called multiple times, and some of that calls with a default value.

Here I tried your example in a simplified version and got a crash:

Then I added a default Book and it started to work:

This became especially noticeable when, in Xcode 16.2, the @Entry macro was changed, and it started generating code with a computed property for defaultValue instead of a stored property.


Used code:

import SwiftUI

struct Book {
    // Interesting properties here...
    let title: String

    static func createBook(title: String) async -> Book {
        // Await the content here...
        // Initialize the Book with actual content...
        return Book(title: title)
    }
}

extension EnvironmentValues {
    @Entry var previewBook: Book = {
//        let bookTitle = "default book to avoid crashes in preview"
//        print(bookTitle)
//        return Book(title: bookTitle)
        // This is the default value and will only be used if you forget to set your preview modifier
        // trait.
        fatalError("No value set for previewBook in the environment, did you forget your preview modifier trait?")
    }()
}

extension View {
    func previewBook(_ book: Book) -> some View {
        environment(\.previewBook, book)
    }
}

struct BookContent: PreviewModifier {
    static func makeSharedContext() async throws -> Book {
        // This async throws function is called to create the context for the preview modifer
        // that can then be applied to preview content.
        return await Book.createBook(title: "Example")
    }

    // This is called to modify your preview content with the context created above whenever
    // you apply your preview modifier to a #Preview.
    func body(content: Content, context: Book) -> some View {
        content.previewBook(context)
    }
}

extension PreviewTrait where T == Preview.ViewTraits {
    // Create a convenience for applying the above preview modifier to a preview
    static var previewBook: Self {
        .modifier(BookContent())
    }
}

struct BookView: View {
    let book: Book

    var body: some View {
        VStack {
            Text(book.title)
                .padding()
        }
    }
}

#Preview(traits: .previewBook) {
    // @Previewable allows us to use normal SwiftUI attributes like @Environment, @State,
    // and @Binding in a preview body
    @Previewable @Environment(\.previewBook) var book

    BookView(book: book)
}
How to await inside SwiftUI a #Preview?
 
 
Q