Preview Assets in Swift Package

In a .xcodeproj I am able to enrich SwiftUI Previews with resources that are stripped during an archive of an app when placed in a “Preview Assets” asset catalog. See Development Assets in Xcode to enrich SwiftUI Previews for more info.

In a Swift Package, I am not aware of an option that resources, specified for a target in a package manifest, can be made conditional based on configuration.

Adding resources to Swift Package to enrich SwiftUI Previews have a negative impact on an app as it bloats its IPA size unnecessarily.

Do I overlook something or are there plans for Swift Packages to support development assets?

5 Likes

I can't talk about future plans for an Xcode centric feature like this, but there's currently no support for development assets in packages.

1 Like

@NeoNacho If Package Manager Conditional Target Dependencies would support configuration conditionals then it would be an option to create a dedicated helper target containing SwiftUI previews and resources for DEBUG configuration. But I guess there are no plans to introduce support for configuration conditionals?

2 Likes

We are using this approach.

Thanks for sharing your approach on this matter, anreitersimon.

I'm doing exactly the same thing - environment variable triggers that are used to manipulate whats in, and out, of Package.swift. Not for this specific use case (preview assets), but the same pattern anyway.

A question to you, @Joseph_Heck, out of curiosity: you are doing this for an internal Swift package (to modularize an iOS app) or for an open-source reusable Swift package? Using environment variable seems fine for an internal Swift package but not for an SDK-style Swift package

I'm using it within an open-source package, specifically to expose a CLI exec that I use when developing the package - generating images for docs in one case, or enabling a benchmarking executable in another. I mostly came at it from the area of not wanting these additional targets "clogging up" a developer's experience when they go to "just use" the package.

1 Like

Interesting, thanks for sharing more lights on how you use this technique of environment variables.

1 Like

In the SE-0273 Acceptance thread, @NeoNacho stated that…

@NeoNacho: Was this effort tracked in a new proposal or other visible location? If not, can you give us an update regarding the current direction? Apologies if there's a discussion that I missed... I've spent a fair bit of time searching to no avail, and it's been quite some time since that quote. Thanks!

From what I see in the code, there's a TargetDependencyCondition type.

Sure, but unless there's something I'm missing, you can only create a TargetDependencyCondition for a collection of platforms, not build configurations (the scope of this thread).

1 Like

Did you manage to get this to work? I now add a preview target but that is combursome too

@doozMen How does adding a preview target work? Can you preview using classes/resources from this target and not have the stuff in that target linked in the final build?

you have 2 targets. I production and one preview. If you use the preview then you have the resources linked in your app. but the production does not

package = Package {
products: [
.library("FooProduction"),
.library("FooPreview")
],
targets: [
.target("FooProduction"), // no dependencies and/or resources here
.target("FooPreview",  dependencies: ["some"],  resources: ["some"])
]

It is a pain as a bit of a duplicate effort but it works.

I hit this issue today. It would be great if SPM could have better support for development assets.
I ended up resorting to a specific package to contain all the dev assets and exporting those using public accessors.
As the last step of the build process, I delete the associated resource bundle from the build output (yeah, a bit hacky).
Those assets shouldn't be referenced from prod code but to protect myself from unexpected crashes in prod from referencing them, I wrapped the assets public accessors with #if DEBUG. This cascades down the code and previews using those assets which is a little inconvenient but I feel a little safer that way.
It's a bit annoying though that debug builds might work but release builds might not, detecting this issue later in the CI pipeline. But that should be better than potential crashes in prod.

1 Like

Just for the record, I ended up refining the above approach. This is for a macOS app, not sure if this will work for other platforms. This was it:

  • A single package named SharedPreviewAssets that contains all preview assets
  • The package exports assets using public constants as described in the docs or this WWDC video

Example:

public enum SharedPreviewAssets {
    public static let background = Image(.background)

    // These assets might not be found in prod so even if these resources shouldn’t be accessed by production code, we make sure we don’t crash just in case
    // The generated SwiftUI ImageResource Image extension won’t crash but the extension of NSImage will crash so we do it this way.
    public static let someNSImage = Bundle.module.image(forResource: “image”) ?? placeholderNsImage

    /// Placeholder image to return when the asset was not found
    private static let placeholderNsImage = NSImage(size: NSSize(width: 1, height: 1))
}
  • This package must be included in the Package.swift of clients as a dependency. Then it can be used this way:
import SharedPreviewAssets

struct MyView: View {
    var body: some View { ... }
}

#Preview {
    MyView()
    .background(SharedPreviewAssets.background)
}
  • A Run Script build phase at the end will delete the preview resources in the build output of the app. Note I didn’t delete the whole bundle but only its resources content. This way it shouldn’t crash. The bundle is still there at the final build but at least it is empty.
#!/bin/bash

echo "Cleaning up preview assets..."
PREVIEW_ASSETS_PATH="${TARGET_BUILD_DIR}/${WRAPPER_NAME}/Contents/Resources/SharedPreviewAssets_SharedPreviewAssets.bundle/Contents/Resources"

if [ ! -d "$PREVIEW_ASSETS_PATH" ]; then
    echo "Preview assets directory not found: $PREVIEW_ASSETS_PATH"
    exit 0
fi

# Check if the directory has any files before deleting
if [ -z "$(ls -A "$PREVIEW_ASSETS_PATH")" ]; then
    echo "No preview assets found to be deleted"
    exit 0
fi
    
# Proceed to delete if files are present
rm -rf "${PREVIEW_ASSETS_PATH}"/*
echo "Preview assets deleted successfully"

I didn’t end up using #if DEBUG. This way the developer experience feels a bit better and it should still be safe. This also prevents having build issues on release if you forget wrapping all previews with the compiler directive. If you do what you shouldn’t do, i.e, referencing preview assets from prod code, it will not load the assets but at least it won’t crash.

Again, not ideal and a bit hacky but seems promising in the meantime. It would be great if proper developer assets support is added to SPM.

Thanks!