In SwiftUI #Preview is crashing when using @Environment property wrapper (Observable Macro)

276 Views Asked by At

I am using the Observable macro and when I use @Environment property wrapper to instance my model the preview stop working. Sample code below

import SwiftUI
import Observation

@Observable class Library {
    // ...
}

Now in my main app I created an instance of Library and add that instance to the environment

@main
struct BookReaderApp: App {
    @State private var library = Library()


    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(library)
        }
    }
}

Now if I want to retrieve the Library instance from any view using the @Environment property wrapper the preview stop working completely (Assume the BoolView actually exist)

struct LibraryView: View {
    @Environment(Library.self) private var library
    
    var body: some View {
        List(library.books) { book in
            BookView(book: book)
        }
    }
}

#Preview {
    LibraryView()
}

If I go back and don't use the Observable macro and just use the ObservableObject with @StateObject, .environemntObject and @EnvironmentObject the #Preview work fine.

This Observable macro is "related" new. Any idea why this is happening? Is there any workaround? I am working with Xcode Version 15.2. Thanks in advance for any kind of help!

2

There are 2 best solutions below

0
ScottM On BEST ANSWER

When you declare an observable class using @Environment(Type.self)..., you are implicitly telling the view that the environment object will be present. However, in your preview you're not providing one, and the crash happens when the view tries to access the non-existent object.

There's no need to create a separate EnvironmentKey, as this goes against the ethos of the Observable environment feature. All you need to do is provide an environment copy in your preview.

The following would be sufficient:

#Preview {
    LibraryView()
        .environment(Library())
}

However, it might be worth creating a dummy example object with values that would demonstrate your view's capabilities, e.g.:

extension Library {
  static var example: Library = Library(books: [/* ... */])
}

#Preview {
  LibraryView()
    .environment(Library.example)
}
4
malhal On

For previewing you can use the preview version of your library, e.g.

#Preview {
    LibraryView()
        .environment(Library.preview)
}

There is however a mistake in your App code. When @State is an @Observable class it needs to be optional and init afterwards like in onAppear. This is because @State inits its default value every time the View is init with is fine if its a value but a major memory leak if its an object being init on the heap.

@main
struct BookReaderApp: App {
    @State private var library: Library?

    var body: some Scene {
        WindowGroup {
            if let library {
                LibraryView()
                    .environment(library)
            }
            else {
                Text("Library not init")
                    .onAppear {
                        library = Library()
                    }
            }
        }
    }
}

You can read more info about this in the docs here: https://developer.apple.com/documentation/swiftui/state

Store observable objects You can also store observable objects that you create with the Observable() macro in State; for example:

@Observable class Library {
    var name = "My library of books"
    // ... }


struct ContentView: View {
    @State private var library = Library()


    var body: some View {
        LibraryView(library: library)
    }
}

A State property always instantiates its default value when SwiftUI instantiates the view. For this reason, avoid side effects and performance-intensive work when initializing the default value. For example, if a view updates frequently, allocating a new default object each time the view initializes can become expensive. Instead, you can defer the creation of the object using the task(priority:_:) modifier, which is called only once when the view first appears: struct

ContentView: View {
    @State private var library: Library?


    var body: some View {
        LibraryView(library: library)
            .task {
                library = Library()
            }
    } 
}

Delaying the creation of the observable state object ensures that unnecessary allocations of the object doesn’t happen each time SwiftUI initializes the view. Using the task(priority:_:) modifier is also an effective way to defer any other kind of work required to create the initial state of the view, such as network calls or file access.