Enables recording of views in Swift packages that need a key window.
This is useful for UI that does not render correctly in a package-only snapshot test, such as:
.sheet.fullScreenCover.alert- confirmation dialogs
- anything else that needs a real key window
- views containing a
Togglecontrol from iOS 26 onward
You are a good citizen and split your app into multiple packages. However, when you snapshot the views in the package, you notice something odd.
@Test
func testClosedState() {
let sut = SheetView(isSheetPresented: true, toggleOn: false)
assertSnapshot(of: sut, as: .image(layout: .device(config: .iPhone13)))
}The snapshot does not show the sheet and, from iOS 26 onward, the Toggle has no content 🤔
The problem: the view needs a key window in order to show the sheet and properly draw the toggle.
You could move all tests to the main app project, but that would break your carefully crafted architecture.
@Test(.requiresKeyWindow)
func testSheetViewSheetOpenState() {
let sut = SheetView(isSheetPresented: true, toggleOn: false)
assertHostedSnapshot(of: sut, devices: [("iPhone13", .iPhone13)], wait: 1.0)
}You annotate tests with @Test(.requiresKeyWindow) and call assertHostedSnapshot(...). Et voila:
It is a bit dirty 😅
- A code generator scans your package tests for
@Testmethods tagged with.requiresKeyWindow. - It generates a new Swift Testing suite in your host app test target.
- The generated tests run inside the app-hosted environment and render snapshot images with a real key window.
verifySnapshot(...)from swift-snapshot-testing compares those images to references.- If no reference image exists, the new image is written back to the package's
__Snapshots__folder next to your regular package snapshots.
You can tell from this description that the process is brittle. But you cannot have your cake and eat it too.
Add the package to both places:
- the package under test
- the host app Xcode project
.package(url: "https://github.com/teufelaudio/HostedSnapshotHelper.git", from: "0.0.1")Add HostedSnapshotHelper to the package that owns your Swift Testing snapshot tests.
Example:
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "FeaturePackage",
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.0.0"),
.package(url: "https://github.com/teufelaudio/HostedSnapshotHelper.git", from: "0.0.1"),
],
targets: [
.target(name: "Feature"),
.testTarget(
name: "FeatureTests",
dependencies: [
"Feature",
.product(name: "HostedSnapshotHelper", package: "HostedSnapshotHelper"),
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
]
),
]
)Keep ordinary snapshots unchanged:
import SnapshotTesting
import Testing
import Feature
@MainActor
struct FeatureSnapshotTests {
@Test
func testClosedState() {
let sut = FeatureView()
assertSnapshot(
of: sut,
as: .image(layout: .device(config: .iPhoneSe))
)
}
}For key-window cases, tag the test with @Test(.requiresKeyWindow) and use assertHostedSnapshot(of:):
import SnapshotTesting
import Testing
import Feature
import HostedSnapshotHelper
@MainActor
struct FeatureSnapshotTests {
@Test(.requiresKeyWindow)
func testSheetOpenState() {
let sut = FeatureView(isSheetPresented: true)
assertHostedSnapshot(of: sut)
}
}You can customize hosted assertions much like assertSnapshot(...) wrappers:
assertHostedSnapshot(
of: sut,
devices: [
("iPhoneSE3rdGen", .iPhoneSE3rdGen),
("iPadPro12_9", .iPadPro12_9(.portrait)),
],
style: [.light, .dark],
wait: 0.5,
named: "dark",
record: nil,
timeout: 5
).requiresKeyWindow is a custom Testing trait provided by this package.
In package tests, it behaves like this:
- disable hosted tests by default
- keep them discoverable by the generator
If you want to run those package tests directly anyway, set:
RUN_HOSTED_PACKAGE_TESTS=1You can also provide a custom disable message:
@Test(.requiresKeyWindow("Hosted snapshots run in the app-hosted test suite."))
func testSheetOpenState() {
let sut = FeatureView(isSheetPresented: true)
assertHostedSnapshot(of: sut)
}Add the package to the host app Xcode project and link HostedSnapshotHelper in the host app test target.
The host app test target must also be able to import the package under test.
Typical setup:
- app target imports the feature package normally
- app test target links
HostedSnapshotHelper - generated Swift Testing host file is written into the app test target's source directory
It is recommended to create a test plan that includes both:
- tests from the Swift package
- tests from the host app
Add a Run Script build phase so the host app regenerates hosted tests before running tests.
Example:
set -eu
OUTPUT_DIR="${SRCROOT}/MyAppTests"
DEPENDENCIES_FILE_LIST="${SRCROOT}/MyAppTests/HostedSnapshotDependencies.xcfilelist"
PACKAGE_ROOT="${SRCROOT}/FeaturePackage"
HELPER_ROOT="${SRCROOT}/HostedSnapshotHelper"
env -u SDKROOT -u PLATFORM_NAME -u EFFECTIVE_PLATFORM_NAME -u ARCHS \
xcrun --sdk macosx swift run \
--package-path "$HELPER_ROOT" \
HostedSnapshotRegistryGenerator \
--package "$PACKAGE_ROOT" \
--output-dir "$OUTPUT_DIR" \
--dependencies-file-list "$DEPENDENCIES_FILE_LIST"Notes:
- use
xcrun --sdk macosx swift runThe generator is a macOS host tool, not an iOS binary. - unset
SDKROOT,PLATFORM_NAME,EFFECTIVE_PLATFORM_NAME, andARCHSXcode can leak iOS build settings intoswift run, which breaks SwiftPM manifest evaluation. - write generated files into the host app test target directory via
--output-dir(<PackageName>HostedSnapshotTests.generated.swift) - pass
--dependencies-file-listto emit an.xcfilelistcontaining all tagged test source files (@Test(.requiresKeyWindow)) - add
$(SRCROOT)/MyAppTests/HostedSnapshotDependencies.xcfilelistto the build phase Input File Lists so the script phase only reruns when those tagged files change - if you integrate
HostedSnapshotHelperas a remote Xcode package instead of a sibling checkout,HELPER_ROOTis typically:"$SOURCEPACKAGES_DIR_PATH/checkouts/HostedSnapshotHelper"
The generator scans Swift files in the package and looks for tests under Tests that are tagged with .requiresKeyWindow.
For each hosted test, it requires:
- exactly one
assertHostedSnapshot(...)call - a self-contained test body that can be replayed in the host app test target
It preserves:
- the test body
- non-
Testingimports from the source file - top-level support declarations (for example helper functions and extensions)
- the original package snapshot directory
- the original package test name
That last point is important: hosted snapshots are rendered in the app, but saved in the package's snapshot folder.
- each tagged hosted test must contain exactly one
assertHostedSnapshot(...)call - tagged tests should avoid wrappers that hide or duplicate hosted assertions
- imports used by tagged package tests must also be valid in the host app test target
A full working example lives in Example/:
Example/LocalExamplePackageis the package under testExample/SnapshotsInPackagesis the host appExample/SnapshotsInPackagesTests/LocalExamplePackageHostedSnapshotTests.generated.swiftis generated from tagged package tests

