swiftswiftuiswiftui-previewsswift-macro

SwiftUI Preview macro not working with Observation


Xcode Version 15.0 (15A240d) IOS 17 Workstation - MacBook Pro 14 inch 2021 with M1 Pro CPU

Summary: I have been using the Observable Object protocol with no issues, including previews. But now with the introduction of the #Preview macro and the Observation framework I can't get previews to work.

Here is a snippet of working preview code. Note this is using the Observation framework.

struct ContentView: View {
    @Environment(Banana.self) private var banana

    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world! \(banana.ripe)")
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var banana: Banana = Banana()
    static var previews: some View {
        ContentView()
            .environment(banana)
    }
}

Here is the same code but with the Preview macro, that does not work.

struct ContentView: View {
    @Environment(Banana.self) private var banana

    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world! \(banana.ripe)")
        }
        .padding()
    }
}

#Preview {
    var banana: Banana = Banana()
    ContentView()
        .environment(banana)
}

The Banana class looks like this:

@Observable class Banana {
    var ripe: String = "sort of"
}

This code produces a warning Result of call to 'environment' is unused and then crashes preview.

Here is some of the error log

== PREVIEW UPDATE ERROR:

    SchemeBuildError: Failed to build the scheme ”PreviewHelp”
    
    ambiguous use of 'init(_:traits:body:)'
    
    Compile ContentView.swift (arm64):
    /Users/isaaccarrington/Development/PreviewHelp/PreviewHelp/ContentView.swift:35:10: warning: result of call to 'environment' is unused
            .environment(banana)
             ^          ~~~~~~~~
    @__swiftmacro_11PreviewHelp33_8AE7F6CA27A0D27F1AD251A98894DACELl0A0fMf_.swift:8:9: error: ambiguous use of 'init(_:traits:body:)'
            DeveloperToolsSupport.Preview {
            ^
    /Users/isaaccarrington/Development/PreviewHelp/PreviewHelp/ContentView.swift:32:1: note: in expansion of macro 'Preview' here
    #Preview {
    ^~~~~~~~~~
    /Users/isaaccarrington/Development/PreviewHelp/PreviewHelp/ContentView.swift:32:1: note: in expansion of macro 'Preview' here
    #Preview {
    ^~~~~~~~~~
    UIKit.Preview:3:12: note: found this candidate
        public init(_ name: String? = nil, traits: PreviewTrait<Preview.ViewTraits>..., body: @escaping @MainActor () -> UIView)
               ^
    UIKit.Preview:4:12: note: found this candidate
        public init(_ name: String? = nil, traits: PreviewTrait<Preview.ViewTraits>..., body: @escaping @MainActor () -> UIViewController)
               ^
    SwiftUI.Preview:3:12: note: found this candidate
        public init(_ name: String? = nil, traits: PreviewTrait<Preview.ViewTraits>..., body: @escaping @MainActor () -> View)

Expectation is that the preview shows.


Solution

  • #Preview { ... } does not work like a ViewBuilder. When it is expanded, the code you put in { ... } gets placed into a closure and then passed to Preview.init(_:traits:body:). The body parameter of this init is not a ViewBuilder. It is just a regular () -> View:

    init(
        _ name: String? = nil,
        traits: PreviewTrait<Preview.ViewTraits>...,
        body: @escaping @MainActor () -> View
    )
    

    Therefore, the closure you passed is invalid, because you don't return anything.

    Compare this to the old PreviewProvider.previews property, which is a ViewBuilder, allowing you to put multiple views in there to show multiple previews. With the #Preview macro there is no need for it to be a ViewBuilder, because to show multiple previews, you can just write #Preview { ... } as many times as you want.

    If you remove the var and just do:

    #Preview {
        ContentView().environment(Banana())
    }
    

    Then the closure entirely consists of one expression, which is implicitly returned.

    If you really need the var for some reason, you can explicitly return the final view you create:

    #Preview {
        var banana = Banana()
        return ContentView()
            .environment(banana)
    }
    

    Or, put the whole thing in a Group. Group.init does take a ViewBuilder:

    #Preview {
        Group {
            var banana = Banana()
            ContentView()
                .environment(banana)
        }
    }