Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions Docs/Preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

OpenSwiftUI declares a `Preview` macro overload whose view builder produces `OpenSwiftUI.View`. UIKit and AppKit continue to provide the controller-oriented Preview overloads. When these modules are imported together, Swift selects the matching macro from the type produced by the body:

1. `#Preview` with an `OpenSwiftUI.View` body resolves to OpenSwiftUI's macro and collects the content in a `Group`
2. `_previewVC()` wraps that content in `NSHostingController` or `UIHostingController`
3. The expansion emits the `DeveloperToolsSupport.PreviewRegistry` that Xcode discovers and returns a preview containing the resulting controller
1. `#Preview` with an `OpenSwiftUI.View` body resolves to OpenSwiftUI's macro
2. The expansion emits the `DeveloperToolsSupport.PreviewRegistry` that Xcode discovers. Like SwiftUI's expansion, `makePreview()` applies `ViewBuilder` to the body through a local `__b_buildView(body:)` function
3. The result goes to OpenSwiftUI's `Preview.init(_:traits:body:)` overload, which takes an `OpenSwiftUI.View` body

```swift
import AppKit
Expand All @@ -27,7 +27,7 @@ import OpenSwiftUI

The first declaration uses OpenSwiftUI's Preview macro because its body produces an `OpenSwiftUI.View`. The second continues to use AppKit's Preview macro because its body produces an `NSViewController`. The same overload selection applies to UIKit and `UIViewController`; these common cases do not require explicit module qualification.

Native macOS targets return an `NSViewController`; iOS, Mac Catalyst, and visionOS targets return a `UIViewController`. The current hosting-controller path does not support tvOS or watchOS because OpenSwiftUI does not provide a hosting controller on those platforms.
The initializer puts the view in an `NSHostingController` on macOS, or a `UIHostingController` on iOS, Mac Catalyst, and visionOS. It then wraps the controller in a SwiftUI view and uses SwiftUI's view Preview initializer. Xcode applies the platform canvas to SwiftUI previews only, for example the visionOS window canvas and camera controls. The current path does not support tvOS or watchOS because OpenSwiftUI does not provide a hosting controller on those platforms.

The macro accepts the same `traits:` and additional trait arguments as SwiftUI's view Preview macro.

Expand All @@ -43,17 +43,20 @@ Xcode Preview's XOJIT loading model requires both `OpenSwiftUI.framework` and `O

Link `OpenSwiftUICore` with `-ObjC` when building the framework so Objective-C categories from the static `OpenSwiftUI_SPI` dependency are retained in the Core dylib. This is a framework-build setting; consumers of the resulting XCFrameworks do not need to add `-ObjC`.

Consumers that integrate the XCFrameworks directly instead of using `OpenSwiftUI-spm` must provide the matching macro plugin separately. Without it, they can still wrap the view with `_previewVC()` explicitly so overload resolution selects the platform Preview macro. For example, on macOS:
Consumers that integrate the XCFrameworks directly instead of using `OpenSwiftUI-spm` must provide the matching macro plugin separately. Without it, they can declare a `PreviewProvider` instead:

```swift
import AppKit
import OpenSwiftUI

#Preview("HostingVC") {
ContentView()._previewVC()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
```

The `_previewVC()` helper is deprecated. It wraps the view in a hosting controller for the UIKit and AppKit Preview macros, so it does not get the visionOS window canvas.

### The `waitingForPreviewThunks` Problem

In Xcode Preview mode, SwiftUI gates graph instantiation behind a `waitingForPreviewThunks` flag (checked via `XCODE_RUNNING_FOR_PREVIEWS` env var). After all preview dylibs are loaded, `PreviewsInjection.framework` calls `SwiftUI.__previewThunksHaveFinishedLoading()` to unblock graph hosts.
Expand Down Expand Up @@ -104,8 +107,8 @@ sequenceDiagram
The current shim-based approach is a temporary bridge. The long-term goal is for OpenSwiftUI to have its **own** native preview support:

### Phase 1: Native OpenSwiftUI Preview Expansion
- Replace the current hosting-controller registry body with one that previews `OpenSwiftUI.View` directly
- Eliminate the internal `_previewVC()` shim and hosting controller wrapper
- Replace the hosting controller inside `Preview.init(_:traits:body:)` with a preview that renders `OpenSwiftUI.View` directly
- Remove the deprecated `_previewVC()` shim and the hosting controller wrapper
- Keep the `#Preview { MyView() }` source interface while changing its implementation

### Phase 2: Own Preview Thunk Registration
Expand Down
3 changes: 0 additions & 3 deletions Sources/COpenSwiftUI/Util/PreviewProvider.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
#include "OpenSwiftUIBase.h"

#if OPENSWIFTUI_TARGET_OS_DARWIN
#include <TargetConditionals.h>
#if TARGET_OS_OSX || TARGET_OS_IOS || TARGET_OS_VISION

#import <Foundation/Foundation.h>
#include <dlfcn.h>
Expand Down Expand Up @@ -385,5 +383,4 @@ + (void)load {

#undef PREVIEW_PTRAUTH
#endif /* __arm64__ || __x86_64__ */
#endif /* TARGET_OS_OSX || TARGET_OS_IOS || TARGET_OS_VISION */
#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ public struct AccessibilityChildBehavior: Hashable {
for index in children.indices where children[index].platformElement != nil {
let bridgedProperties = Graph.withoutUpdate {
childProperties[index].bridgedElement.map { element -> AccessibilityProperties in
#if os(macOS) || os(iOS) || os(visionOS)
#if canImport(Darwin)
element.bridgedProperties
#else
_openSwiftUIPlatformUnimplementedFailure()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ extension AccessibilityAttachment {
var properties = properties
let bridgedProperties = Graph.withoutUpdate {
properties.bridgedElement.map { element -> AccessibilityProperties in
#if os(macOS) || os(iOS) || os(visionOS)
#if canImport(Darwin)
element.bridgedProperties
#else
_openSwiftUIPlatformUnimplementedFailure()
Expand Down
4 changes: 2 additions & 2 deletions Sources/OpenSwiftUI/Accessibility/AccessibilityFocus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ struct AccessibilityFocus {
if let node = platformAccessibilityElement.accessibilityNodeForPlatformElement {
return node
}
#if os(iOS) || os(visionOS) || os(macOS)
#if canImport(Darwin)
var node: AccessibilityNode?
platformAccessibilityElement.traverseAncestors { element in
if let ancestor = element as? AccessibilityNode {
Expand Down Expand Up @@ -70,7 +70,7 @@ struct AccessibilityFocus {
ancestor == focusStoreNode {
return .platformChildFocused
}
#if os(iOS) || os(visionOS) || os(macOS)
#if canImport(Darwin)
if let ancestor = ancestorAccessibilityNode {
var isContainerChildFocused = false
ancestor.traverseAncestors { element in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import OpenSwiftUI_SPI
struct AccessibilityRole {
var resolved: Resolved

#if !os(macOS) && !os(iOS) && !os(visionOS)
#if !canImport(Darwin)
struct Resolved {
var traits: AXOpenSwiftUITraits
var automationType: AXAutomationType?
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenSwiftUI/App/App/Stdout/StdoutApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func runStdoutApp(
print("OpenSwiftUI stdout renderer: no scene to render")
return
}
#if os(macOS) || os(iOS) || os(visionOS)
#if canImport(Darwin)
let rootView = item.value.view
.frame(width: options.surface.width, height: options.surface.height)
.rootEnvironment(scenePhase: .active, sceneID: item.id)
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenSwiftUI/Event/Focus/FocusItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ private struct PlatformSubtreeIterator: Sequence, IteratorProtocol {
var count: Int = 0

mutating func next() -> PlatformView? {
#if os(iOS) || os(visionOS) || os(macOS)
#if canImport(Darwin)
#if os(iOS) || os(visionOS)
guard count < 10 else { return nil }
#else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Audited for 6.5.4
// Status: Complete

#if os(iOS) || os(visionOS) || os(macOS)
#if canImport(Darwin)
@_spi(ForOpenSwiftUIOnly)
import OpenSwiftUICore

Expand Down
8 changes: 4 additions & 4 deletions Sources/OpenSwiftUI/Integration/LegacyPreviewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// LegacyPreviewProvider.swift
// OpenSwiftUI

#if canImport(SwiftUI) && (os(macOS) || os(iOS) || os(visionOS))
#if canImport(Darwin)
import OpenSwiftUICore
import SwiftUI

Expand Down Expand Up @@ -46,7 +46,7 @@ private class LegacyPreviewProviderWitness: SwiftUI.PreviewProvider {

private static func nativePreview(_ preview: OpenSwiftUI._Preview) -> SwiftUI.AnyView {
var view = SwiftUI.AnyView(
LegacyPreviewView(content: preview.content)
NativePreviewView(content: preview.content)
.previewDisplayName(preview.displayName)
.previewDevice(preview.device.map { SwiftUI.PreviewDevice(rawValue: $0.rawValue) })
.previewLayout(preview.layout)
Expand Down Expand Up @@ -93,7 +93,7 @@ private struct LegacyPreviewContext: OpenSwiftUI.PreviewContext, SwiftUI.Preview
}

#if (os(iOS) || os(visionOS))
private struct LegacyPreviewView: SwiftUI.UIViewControllerRepresentable {
struct NativePreviewView: SwiftUI.UIViewControllerRepresentable {
var content: OpenSwiftUI.AnyView

func makeUIViewController(context: Context) -> OpenSwiftUI.UIHostingController<OpenSwiftUI.AnyView> {
Expand All @@ -118,7 +118,7 @@ private struct LegacyPreviewView: SwiftUI.UIViewControllerRepresentable {
}
}
#elseif os(macOS)
private struct LegacyPreviewView: SwiftUI.NSViewControllerRepresentable {
struct NativePreviewView: SwiftUI.NSViewControllerRepresentable {
var content: OpenSwiftUI.AnyView

func makeNSViewController(context: Context) -> OpenSwiftUI.NSHostingController<OpenSwiftUI.AnyView> {
Expand Down
34 changes: 30 additions & 4 deletions Sources/OpenSwiftUI/Integration/PreviewShims.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,51 @@
// OpenSwiftUI
//
// Bridging helpers for Xcode Preview support.
// Wraps OpenSwiftUI views in hosting controllers so they can be used with
// AppKit and UIKit's #Preview macros.
// Presents OpenSwiftUI views as SwiftUI previews for the #Preview macro.

import OpenSwiftUICore

// Helper method before we add native preview thunk support for OpenSwiftUI.

#if canImport(AppKit) && !targetEnvironment(macCatalyst)
public import AppKit
extension OpenSwiftUI.View {
@available(*, deprecated, message: "Use the OpenSwiftUI #Preview macro or PreviewProvider instead.")
public func _previewVC() -> NSViewController {
OpenSwiftUI.NSHostingController(rootView: self)
}
}
#elseif canImport(UIKit)
public import UIKit
extension OpenSwiftUI.View {
@available(*, deprecated, message: "Use the OpenSwiftUI #Preview macro or PreviewProvider instead.")
public func _previewVC() -> UIViewController {
OpenSwiftUI.UIHostingController(rootView: self)
}
}
#endif

#if canImport(DeveloperToolsSupport)
public import DeveloperToolsSupport
import SwiftUI

@available(iOS 17.0, macOS 14.0, visionOS 1.0, *)
extension DeveloperToolsSupport.Preview {
/// Creates a preview of an OpenSwiftUI view.
///
/// - Parameters:
/// - name: An optional display name for the preview.
/// - traits: An optional list of traits that customize the preview.
/// - body: A closure that produces the OpenSwiftUI view to preview.
@MainActor
public init(
_ name: String? = nil,
traits: PreviewTrait<Preview.ViewTraits>...,
body: @escaping @MainActor () -> any OpenSwiftUI.View
) {
// Swift cannot forward a variadic list, so combine the traits first.
let trait = traits.reduce(PreviewTrait()) { PreviewTrait($0, $1) }
self.init(name, traits: trait) {
NativePreviewView(content: OpenSwiftUI.AnyView(body()))
}
}
}
#endif
6 changes: 2 additions & 4 deletions Sources/OpenSwiftUI/Macro/PreviewMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import OpenSwiftUICore

/// Creates an Xcode preview from an OpenSwiftUI view.
///
/// The macro hosts the view in a platform view controller and registers it with
/// Xcode Previews.
/// The macro registers the view with Xcode Previews.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@freestanding(declaration)
public macro Preview(
Expand All @@ -22,8 +21,7 @@ public macro Preview(

/// Creates an Xcode preview with traits from an OpenSwiftUI view.
///
/// The macro hosts the view in a platform view controller and registers it and
/// its traits with Xcode Previews.
/// The macro registers the view and its traits with Xcode Previews.
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
@freestanding(declaration)
public macro Preview(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public struct BorderedButtonStyle: PrimitiveButtonStyle {
}

public func makeBody(configuration: Configuration) -> some View {
#if os(macOS) || os(iOS) || os(visionOS)
#if canImport(Darwin)
Button(configuration)
#if os(iOS) || os(visionOS)
.modifier(StaticIf(
Expand Down
43 changes: 18 additions & 25 deletions Sources/OpenSwiftUIMacros/PreviewMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ package struct PreviewMacro: DeclarationMacro {
if let body = node.trailingClosure {
trailingClosure = wrappedBody(body)
} else if let bodyIndex = arguments.lastIndex(where: { $0.label?.text == "body" }) {
arguments[bodyIndex].expression = ExprSyntax(wrappedBody(arguments[bodyIndex].expression))
arguments[bodyIndex].expression = wrappedBody(arguments[bodyIndex].expression)
trailingClosure = nil
} else {
throw MacroExpansionErrorMessage("#Preview requires a view body")
Expand Down Expand Up @@ -71,40 +71,33 @@ package struct PreviewMacro: DeclarationMacro {
return [registry]
}

private static func wrappedBody(_ body: ExprSyntax) -> ClosureExprSyntax {
private static func wrappedBody(_ body: ExprSyntax) -> ExprSyntax {
// A referenced body already matches the OpenSwiftUI Preview initializer.
guard let closure = body.as(ClosureExprSyntax.self) else {
// A referenced body returns an existential View, so erase its
// result to a concrete root before creating the hosting controller.
let wrappedBody: ExprSyntax = """
{
OpenSwiftUI::AnyView((\(body))())
._previewVC()
}
"""
return wrappedBody.as(ClosureExprSyntax.self)!
return body
}
return wrappedBody(closure)
return ExprSyntax(wrappedBody(closure))
}

private static func wrappedBody(_ body: ClosureExprSyntax) -> ClosureExprSyntax {
// Apply the view builder to the statements without an extra root view.
let buildView: DeclSyntax =
"""
func __b_buildView(@OpenSwiftUI::ViewBuilder body: () -> any OpenSwiftUI::View) -> any OpenSwiftUI::View {
body()
}
"""
let content = ClosureExprSyntax(statements: body.statements)
let group = FunctionCallExprSyntax(
callee: DeclReferenceExprSyntax(
moduleSelector: ModuleSelectorSyntax(moduleName: .identifier("OpenSwiftUI")),
baseName: .identifier("Group")
),
trailingClosure: content
)
let previewVC = FunctionCallExprSyntax(
callee: MemberAccessExprSyntax(
base: group,
period: .periodToken(leadingTrivia: .newline),
declName: DeclReferenceExprSyntax(baseName: .identifier("_previewVC"))
let returnBuiltView = ReturnStmtSyntax(
expression: FunctionCallExprSyntax(
callee: DeclReferenceExprSyntax(baseName: .identifier("__b_buildView")),
trailingClosure: content
)
)
var wrappedBody = body
wrappedBody.statements = CodeBlockItemListSyntax([
CodeBlockItemSyntax(item: .expr(ExprSyntax(previewVC)))
CodeBlockItemSyntax(item: .decl(buildView)),
CodeBlockItemSyntax(item: .stmt(StmtSyntax(returnBuiltView))),
])
return wrappedBody.formatted().cast(ClosureExprSyntax.self)
}
Expand Down
Loading
Loading