Skip to content

Support UIKit bodies in #Preview blocks#130

Merged
obj-p merged 2 commits intomainfrom
worktree-fix-uikit-in-previews
Apr 20, 2026
Merged

Support UIKit bodies in #Preview blocks#130
obj-p merged 2 commits intomainfrom
worktree-fix-uikit-in-previews

Conversation

@obj-p
Copy link
Copy Markdown
Owner

@obj-p obj-p commented Apr 20, 2026

Summary

  • #Preview { ExampleUIView() } blocks failed to compile because the generated bridge wrapped every body in a @ViewBuilder func __previewBody() -> some SwiftUI.View, which rejects UIView/UIViewController returns.
  • New PreviewBridgeSource template emits __PreviewBridge.wrap(_:) with a @ViewBuilder generic overload for SwiftUI.View plus non-generic overloads for UIView/UIViewController that auto-wrap in UIViewRepresentable/UIViewControllerRepresentable. Swift's overload resolution picks the right path from the body's return type — same approach Xcode's first-party #Preview macro uses.
  • Helper is platform-gated at generation time via PreviewPlatform rather than #if canImport(UIKit) because UIKit is visible on macOS SDKs for Catalyst interop. AppKit bodies on macOS remain out of scope.

Why

Reported failure when rendering #Preview { ExampleUIView(deps: deps) } inside withDependencies: error: static method 'buildExpression' requires that 'ExampleUIView' conform to 'View'. The @ViewBuilder wrapper Swift-constrained every preview body to SwiftUI views even though many users write UIKit previews.

Known limitations (same as Xcode's #Preview for UIKit)

  • Multi-statement UIKit bodies require an explicit return — UIKit has no @ViewBuilder equivalent.
  • A UIView subclass that also conforms to SwiftUI.View would be ambiguous; left undocumented to avoid @_disfavoredOverload (an underscore-prefixed compiler-internal attribute).
  • Protocol-existential returns (any UIKitProtocol) won't be auto-wrapped; users can add an explicit representable.

Test plan

  • swift test --filter PreviewsCoreTests — 207 tests green (204 pre-existing + 3 new).
  • New full-pipeline compile tests exercise the actual Swift compiler against platform: .iOS bodies:
    • fullPipelineUIViewBody#Preview { ExampleUIView() }
    • fullPipelineUIViewControllerBody#Preview { ExampleVC() }
    • fullPipelineSwiftUIBodyIOS — regression guard that SwiftUI bodies still compile via the new __PreviewBridge.wrap path.
  • fullPipelineWithIfAvailable and fullPipelineWithMultiStatement still pass unchanged (preserve @ViewBuilder semantics: if #available, leading let, branches with different concrete types).
  • examples/spm/Sources/ToDo/UIKitPreview.swift added as an end-to-end fixture; builds cleanly on macOS (guarded with #if canImport(UIKit)).
  • Manual: run /integration-test against the iOS simulator to render the new UIKit example end-to-end.

🤖 Generated with Claude Code

obj-p and others added 2 commits April 20, 2026 12:23
The generated bridge previously wrapped every preview body in a
@ViewBuilder func __previewBody() -> some SwiftUI.View, which failed
to compile when the body returned a UIView or UIViewController:

    error: static method 'buildExpression' requires that
           'ExampleUIView' conform to 'View'

Xcode's first-party #Preview macro sidesteps this with overloaded body
parameters for View, UIView, and UIViewController. This change adds the
same overload set at the generator layer: the new PreviewBridgeSource
template emits __PreviewBridge.wrap(_:) with a @ViewBuilder generic
overload for SwiftUI views and non-generic overloads for UIView and
UIViewController that auto-wrap in UIViewRepresentable /
UIViewControllerRepresentable. Swift's overload resolution picks the
right wrapping based on the body's return type.

Scope is UIKit-only (iOS). AppKit bodies on macOS remain out of scope
until anyone asks. The helper is platform-gated at generation time
via PreviewPlatform rather than #if canImport(UIKit), because UIKit is
visible on macOS SDKs for Catalyst interop.

Full-pipeline compile tests cover UIView, UIViewController, and a
SwiftUI-on-iOS regression guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The reported failure form was `#Preview { ExampleUIView(deps: deps) }`
wrapped inside `withDependencies`. The existing `fullPipelineUIViewBody`
test uses a zero-arg init, which exercises the same overload path but
doesn't mirror the shape that originally broke. Adds a targeted test
that wraps `ExampleUIView(label:)` in a helper closure — same structural
pattern as the user's `withDependencies` case, without pulling in the
swift-dependencies package as a test dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@obj-p obj-p merged commit 860b43a into main Apr 20, 2026
4 checks passed
@obj-p obj-p deleted the worktree-fix-uikit-in-previews branch April 20, 2026 23:02
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