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?
@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?
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.
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!
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).
@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?
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.
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.
import SwiftUI
public enum PreviewAssetsResolver {
/// Finds `PreviewAssets` folder by walking up from a starting file path.
///
/// - Parameter startFilePath: Usually `#filePath`
/// - Returns: URL to PreviewAssets folder if found
public static func resolve(from startFilePath: String) -> URL? {
let fm = FileManager.default
var current = URL(fileURLWithPath: startFilePath)
.deletingLastPathComponent()
// Walk upward until root or Package.swift
while true {
// 1. Check for PreviewAssets in current directory
let candidate = current.appendingPathComponent("PreviewAssets", isDirectory: true)
if fm.fileExists(atPath: candidate.path) {
return candidate
}
// 2. Stop if we reached Package.swift (project root anchor)
let packageSwift = current.appendingPathComponent("Package.swift")
if fm.fileExists(atPath: packageSwift.path) {
return nil
}
// 3. Move one level up
let parent = current.deletingLastPathComponent()
// Stop if we cannot go further
if parent.path == current.path {
return nil
}
current = parent
}
return nil
}
}
/// Executes a closure with access to the `PreviewAssets` folder.
///
/// The function searches upward from the calling file location (`#filePath`)
/// for a folder named `PreviewAssets`, stopping at `Package.swift`.
///
///
/// # Required structure
///
/// `PreviewAssets` must exist somewhere inside the package root:
///
/// ```text
/// MyPackage/
/// ├── Package.swift
/// ├── Sources/
/// │ └── Feature/
/// │ └── File.swift
/// └── PreviewAssets/
/// ├── image.png
/// └── config.json
/// ```
///
/// The folder is not part of the SPM build and is intended only for previews/dev use.
///
/// - Parameter startFilePath: Source file path (defaults to `#filePath`)
/// - Parameter perform: Closure receiving the resolved assets directory
/// - Returns: Result of the closure
public func withAssets<T>(
from startFilePath: String = #filePath,
perform: (URL) -> T
) -> T {
guard let assetsURL = PreviewAssetsResolver.resolve(from: startFilePath) else {
fatalError("PreviewAssets folder not found in package hierarchy.")
}
return perform(assetsURL)
}
#Preview {
withAssets { url in
return AsyncImage(url: url.appending(path: "test_image.png"))
}
}
Something like this works fine in my case.
I have created the folder "PreviewAssets" that contains the assets at the root level of my SPM package. This folder is not referenced in any way by the Package.swift.
Previews can access files from the host environment, so it is possible to directly reference the URL like in the preview example.