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.
#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)
}
}