Skip to content

Fix .onPreview/.onTest not working in Xcode previews#351

Open
bsudekum wants to merge 1 commit intohmlongco:mainfrom
perplexityai:fix/spm-preview-test-context-resolution
Open

Fix .onPreview/.onTest not working in Xcode previews#351
bsudekum wants to merge 1 commit intohmlongco:mainfrom
perplexityai:fix/spm-preview-test-context-resolution

Conversation

@bsudekum
Copy link

.onPreview and .onTest context factories silently fail in Xcode previews. The mock is registered and found, but never used — the default factory runs instead (often fatalError()), crashing the preview.

Cause: Xcode previews dynamically recompile and inject code, creating duplicate TypedFactory<P,T> types across module boundaries. The as? TypedFactory<P,T> cast returns nil even when both sides are TypedFactory<(), BillingUI> — same generic parameters, different runtime type identity.

Fix

Add a structural function-type cast fallback. When the nominal TypedFactory<P,T> cast fails, cast the underlying closure directly via @Sendable (P) -> T. Function types use structural typing in
Swift, which is immune to the compilation-unit identity mismatch.

  • Registrations.swiftAnyFactory exposes untypedFactory: Any. resolve() falls back to casting the closure when the struct cast fails. Removed #if DEBUG from isPreview/isTest in
    factoryForCurrentContext().
  • Resolver.swift — Same fallback for the global resolver path.
  • Modifiers.swift — Removed #if DEBUG from context registration default: case.

The normal (non-preview) path is unchanged — the nominal cast succeeds first and the fallback is never reached.

Why remove #if DEBUG from preview/test gates

isPreview and isTest are false in release, so context factories never resolve outside debug builds. User code already wraps .onPreview/.onTest in #if DEBUG. The .onDebug context retains its #if DEBUG guard.

Two issues prevented context factories from resolving in previews:

1. Registration and resolution of preview/test contexts were gated
   behind #if DEBUG. Since isPreview and isTest are runtime checks
   (environment variables), they don't need compile-time guards.
   Removed #if DEBUG for preview/test; kept it for debug context.

2. Xcode previews dynamically recompile and inject code, causing
   nominal types (TypedFactory<P,T>) to have different type identity
   across module boundaries. The as? TypedFactory<P,T> cast silently
   fails, so context factories are ignored and the default factory
   runs instead (often fatalError).

   Fix: Added untypedFactory property to AnyFactory that exposes the
   closure as Any. When the nominal struct cast fails, resolution
   falls back to casting the closure using structural function types
   (@sendable (P) -> T), which are identity-independent in Swift.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant