How to get access to a View defined inside a #Preview macro

Is there any way to get programmatic access to the View defined inside a #Preview macro?

I'm trying to do this because we use previews for two purposes: first to visually check that the view we are developing is correct and then to use the view generated by this preview in a visual regression test.

Previously we were doing this like:

struct Component_ConfiguredInACertainWay_Preview: PreviewProvider {
    static var previews: some View {
        ...
    }
}

// and then in the test code
let view = Component_ConfiguredInACertainWay_Preview.previews
// and use this view to compare it to a previously recorded snapshot

But now if you use the #Preview macro:

#Preview {
    ...
}
// gets expanded to something like this
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, xrOS 1.0, *)
struct $s19ComponentProjectName33_4C76AECF8A0CECE32927837A361168DBLl7PreviewfMf_15PreviewRegistryfMu_: DeveloperToolsSupport.PreviewRegistry {
    ...
    static func makePreview() throws -> DeveloperToolsSupport.Preview {
        ...
    }
}

this is, the macro expansion creates struct with a seemingly random name. Is there any way I can get access to this makePreview() func, and is there any way to use this to extract a View to be used in a visual regression test?

Thanks in advance!!

3 Likes

Preview providers do still work so you can keep using them where you need this functionality. But no, there is no way to get access to the view in the preview macro without expanding it manually and keeping the code out where you can access it.

What you put in the #Preview macro doesn't have to be a large, bespoke thing. You could make a view struct that contains all the logic and then put a simple instantiation of that in a #Preview block to see it, but then instantiate it in your flow that consumes it for your regression testing.

2 Likes

I'm in a similar boat. This will of course work, but breaks the point of only having a single declaration for both the preview and the test.

I've been banging my head against the wall on this one and can't seem to find a good solution.

What exactly do you mean by that? Inlining the preview macro deosn't work - Xcode won't show the preview pane. Similarly, it's not possible to create your own declaration macro, say #Snapshot:

#Snapshot {
    SomeView()
}

... that on expansion adds...

#Preview {
    SomeView()
}

When I say "not possible", what I mean is that Xcode won't show the preview pane in this case, either. Even though I can manually expand both macros in the editor, Xcode just won't pick up on the existence of the #Preview.

And of course, there is no way to use any attached macros on the invocation of SomeView(), because that's already in a closure...

EDIT: for the sake of completeness, you also can't attach a macro to the #Preview declaration...

Really seems like dead ends all around.

The #Preview macro is free standing and the body of the closure is what is stuffed in to the preview. You don't have to declare everything inside of there, especially if you want to reuse it.

I understand that. But the reuse is exactly the thing I'm trying to achieve... without having to declare something outside of that #Preview.

Consider the previous situation:

struct SomePreview: PreviewProvider {
    static var previews: some View {
        MyView()
    }
}

compared to...

struct UnnecessaryWrapper: View {
    var body: some View {
        MyView()
    }
}

#Preview {
    UnnecessaryWrapper()
}

Now imagine I have 5 previews instead of one. That's a lot of wrappers. And I still have to write UnnecessaryWrapper() twice in two different files... there is nothing preventing those two from geting desynced on update.

Moreover, I believe Xcode will strip preview-related code from the binary. I don't see how that would happen for this wrapper, since there is nothing really linking it to previews...

If Xcode recognized #Preview when generated by another macro, this problem wouldn't exist.

Ah, I see where you're going with this. Filing a feedback would be helpful for sure.

Regarding your statement about stripping preview related code, Xcode does not do anything special today with any preview code (even the old preview providers). It is very likely that dead code stripping will end up removing previews related bits, including wrapper views used only in previews. But that's up to that optimization step. If you want to guarantee that code does not end up in release you are better off explicitly compiling it out based on build configuration.

Good idea, feedback filed - FB13379803.