#Preview-like macro for creating multiple previews

I'm trying to make a macro PreviewBrands that will take a @ViewBuilder and then apply various modifiers to it, resulting in four outputs (like SwiftUI's Preview but with 4 outputs).

I want to use it like this:

#PreviewBrands("ExampleView", body: {
    ExampleView(onStart: {})
})

Here's my definition:

@freestanding(declaration)
public macro PreviewBrands(
    _ name: String,
    @ViewBuilder body: @escaping @MainActor () -> any View
) = #externalMacro(module: "PreviewBrandsMacro", type: "PreviewBrands")

Then, in the expansion(... for DeclarationMacro I do

        ...

        let brands = [
            "Brand1",
            "Brand2",
            "Brand3",
            "Brand4"
        ]

        // Generate the previews for each brand
        let generatedPreviews = brands.map { brand in
            """
            struct BrandPreview_\(previewName)_\(brand): PreviewProvider {
                static var previews: some View {
                    \(bodyExpr.statements)
                        .environmentObject(Context.\(brand.lowercased())Context)
                        .previewDisplayName("\(brand)")
                }
            }
            """
        }.joined(separator: "\n")

        let result: DeclSyntax =
            """
            #if DEBUG
            \(raw: generatedPreviews)
            #endif
            """

        return [result]

This gives me:
Declaration name 'BrandPreview_CalibrationIntroView_Brand1' is not covered by macro 'PreviewBrands' and so on...

And I can't use: @freestanding(declaration, names: arbitrary) because of:
'declaration' macros are not allowed to introduce arbitrary names at global scope

Is there any way I can achieve what I want here?

I would recommend not trying to make preview macros for Xcode. Even though below I only mention the macro, the same goes for PreviewProvider in how Xcode looks for them.

That's really sad news :disappointed:

I wouldn't leverage macros, or the #Preview macro, but you could accomplish something similar with PreviewSnapshots.

You define an array of configurations and a closure to create the preview for a given configuration, and it will generate a preview for each element in the array. It also provides functions for capturing snapshot tests for each preview in unit tests.

The most basic approach to using it would be to declare the array of configurations directly in the PreviewProvider like this:

struct Brands_Previews: PreviewProvider {
    static var previews: some View {
        snapshots.previews
    }

    static var snapshots: PreviewSnapshots<BrandContext> {
        PreviewSnapshots(
            configurations: [
                .init(name: "Brand 1", state: Context.brand1Context),
                .init(name: "Brand 2", state: Context.brand2Context),
                .init(name: "Brand 3", state: Context.brand3Context),
                .init(name: "Brand 4", state: Context.brand4Context),
            ], configure: { state in
                ExampleView(onStart: {})
                    .environmentObject(state)
            }
        )
    }
}

but if you plan to use the configurations repeatedly you could write an extension on PreviewSnapshot make reuse easier:

extension PreviewSnapshots<BrandContext> {
    static func brands(@ViewBuilder content: () -> some View) -> Self {
        PreviewSnapshots(
            configurations: [
                .init(name: "Brand 1", state: Context.brand1Context),
                .init(name: "Brand 2", state: Context.brand2Context),
                .init(name: "Brand 3", state: Context.brand3Context),
                .init(name: "Brand 4", state: Context.brand4Context),
            ], configure: { state in
                content()
                    .environmentObject(state)
            }
        )
    }
}

struct ExampleView_Previews: PreviewProvider {
    static var previews: some View {
        snapshots.previews
    }
    
    static let snapshots = PreviewSnapshots.brands {
        ExampleView(onStart: {})
    }
}


struct AnotherView_Previews: PreviewProvider {
    static var previews: some View {
        snapshots.previews
    }
    
    static let snapshots = PreviewSnapshots.brands {
        AnotherView()
    }
}
1 Like

Does the context.makeUniqueName(:) method work for you?

You won't get cleanly named types like what you desired - but at least you'll have the types defined for the previews to work.

If you expand the output of the #Preview macro, you'll see a sort of mangled Swift type name as the name of the generated Struct.

1 Like

Clean names doesn't matter that much, since it's not really visible.

Thanks for the tip! I thought I tried that but can't remember it working yesterday, but now it does work!! But - even tho it appears as if it generates those structs correctly, nothing appears on the canvas :( Thanks for the input tho!

Thank you for this input! This is a really good suggestion! One could even go somewhat further and do:

func AllBrands(@ViewBuilder content: @escaping () -> some View) -> some View {
    PreviewSnapshots<Context>.brands {
        content()
    }.previews
}

And then I'd be able to write, each time I want all brands showed:

struct Example_Previews: PreviewProvider {
    static var previews: some View {
        AllBrands {
            ExampleView(onStart: {})
        }
    }
}

I you only want previews you can skip PreviewSnapshots and create a AllBrands View with a ForEach at the root for the difference context, which is basically all the .previews property is doing.

The added benefit of making a separate property for snapshots is that you can write snapshot tests for each combination with a single assertion:

import PreviewSnapshotsTesting
import XCTest

@testable import BrandsModule

final class ExampleViewSnapshotTests: XCTestCase {
    func test_snapshots() {
        ExampleView_Previews.snapshots.assertSnapshots()
    }
}