- Uses real SwiftUI — examples
import SwiftUIand callApp.main() - SwiftOpenUI core library compiles on macOS (for tests) but is not used for rendering
- Namespace conflict:
ObservableObjectandPublishedclash with Foundation/Combine — tests useSwiftOpenUI.ObservableObjectand@SwiftOpenUI.Publishedprefixes (seedocs/issues/observable-namespace-conflict.md) - Package minimum: macOS 13 (required by JavaScriptKit dependency)
SwiftUI example apps launched via swift run can start in the background or create their first
window slightly after App.main() enters the Cocoa lifecycle. The working rule on macOS is:
front the app only after the app has finished launching and a real NSWindow exists.
Mechanism used by MacExampleSupport:
- Call
NSApplication.shared.setActivationPolicy(.regular)beforeApp.main() - Do not replace
NSApplication.shared.delegate - Install passive observers for app and window notifications
- Wait for
NSApplication.didFinishLaunchingNotification - Retry briefly on the main queue because SwiftUI may create the first window a moment later
- When a usable
NSWindowexists, call:NSApplication.shared.activate(ignoringOtherApps: true)window.makeKeyAndOrderFront(nil)window.orderFrontRegardless()
What we learned:
- Setting focus policy too early is ineffective; there is no real window to front yet
- Owning the global app delegate is too invasive; SwiftUI needs to control its own lifecycle
- A small retry loop is acceptable for example apps because window creation is asynchronous
- Logging app and window notifications is much more informative than guessing from launch timing
macOS window sizing has a few separate concerns that should not be collapsed into one flag:
- Default size: the initial size when the window is first created
- Minimum size: the smallest user-resizable size
- Maximum size: the largest user-resizable size
- Resizable or fixed: whether the user can drag-resize at all
- Content-driven size: whether the window should follow its content's ideal size
Practical guidance:
- Treat default size as an initial suggestion, not as a permanent clamp
- Treat min and max as persistent constraints
- For fixed-content examples such as Calculator, content-sized behavior is often the cleanest fit
- If a window should not resize, express that explicitly instead of faking it with
min == max - Keep launch/fronting logic separate from sizing logic; they are different responsibilities
On native macOS implementations, typical knobs are:
- SwiftUI:
WindowGroup,.windowResizability(...), and scene/window sizing modifiers - AppKit:
setContentSize,minSize,maxSize, and theresizablestyle mask
For swift run showcase apps, "quit when the last window closes" is reasonable and feels native.
That policy belongs in the example-launch helper, not in shared view code. It should remain a thin
example-runner behavior, not a global framework rule.
- Backend:
BackendGTK4→ renders to GtkWidgets - Requires:
libgtk-4-dev(sudo apt install libgtk-4-dev) - CSS styling via
CSSHelperfor fonts, colors, borders - Thread-local environment via
pthread_key_t
- Backend:
BackendWin32→ renders to HWNDs with Win32 API + Direct2D - Requires: Visual Studio with Windows SDK
Three-layer design mirroring the GTK4 backend:
| Layer | Target | Purpose |
|---|---|---|
CWin32 |
C/C++ shim | Win32 macro expansions + D2D/DirectWrite COM wrappers |
CWin32Bridge |
Swift bridge | HWNDRef, SubclassHandler, ClosureBox, MainThread |
BackendWin32 |
Rendering | Win32Backend, WinRenderer, Win32ViewHost, LayoutEngine, D2DRenderer |
- HWND-based controls: Text→STATIC, Button→BUTTON — native Win32 controls for standard widgets
- Direct2D: Color fills, Divider lines, and any custom visual rendering (anti-aliased, alpha-aware)
- DirectWrite: Text measurement via
DWriteTextLayout.GetMetrics()— more accurate than GDI'sGetTextExtentPoint32W - Layout: Custom flexbox-like engine using
SetWindowPos()for VStack/HStack/ZStack
Swift's C++ interop (as of Swift 6.2) has a known bug where virtual method calls dispatch statically instead of through the vtable. COM interfaces like ID2D1RenderTarget are pure-virtual — every method must go through vtable dispatch. Calling them directly from Swift invokes the wrong function.
The workaround is d2d1_shim.cpp: a C++ file that wraps each COM call in a extern "C" function. Swift calls the C function, the C++ compiler dispatches through the vtable correctly. The header (d2d1_shim.h) exposes COM objects as opaque struct pointers so Swift gets type-safe distinct types.
When can the shim be removed? When swiftlang/swift#62354 is resolved and Swift can dispatch virtual C++ calls through vtables correctly. At that point, the D2D COM interfaces can be imported directly with import CxxD2D1 or similar.
- Coalesced rebuilds via
PostMessage(WM_SWIFTUI_REBUILD)— Win32 equivalent of GTK'sg_idle_add() - Focus save/restore across rebuilds with
suppressNextFocusRestore()for@FocusState - Owner-draw buttons (
BS_OWNERDRAW+WM_DRAWITEM) for.foregroundColor()on Button controls - Recursive font application via
applyFontRecursively()to reach controls inside modifier wrappers - HFONT leak prevention via cleanup subclass on
WM_NCDESTROY - Thread-local environment via
TlsAlloc/TlsGetValue - Delayed callback environment binding for actions, menus, lifecycle hooks, and gestures that may
read
@Environment(...)after render scope has ended. See Deferred Callback Environment Binding - Gesture routing via recursive subclassing (same proc on root + all descendants, not WM_PARENTNOTIFY)
- Navigation via Win32Navigation.swift — show/hide HWND stack with header bar, thread-local context sharing
- Animation: Timer-driven (SetTimer 60fps + easing) for
OpacityView/ScaleEffectViewon fully D2D-renderable subtrees (Text, Color, Divider, simple wrappers). Easing: linear, easeIn, easeOut, easeInOut, spring.consumePendingAnimation()reads animation set bywithAnimation()after deferred rebuild - Cursor/selection preservation for all Edit controls via
EM_GETSEL/EM_SETSEL(not just focused control) - Canvas: D2D-backed
DrawingContextwith path accumulation, deferred stroke/fill, transform state in save/restore, alpha support. Partial arcs via line-segment approximation (stroke-only). Seedocs/architecture/canvas-parity.mdfor detailed gap analysis - Visual effects:
.cornerRadius()viaSetWindowRgn+CreateRoundRectRgn;.shadow()via layered GDI rects with alpha blending against system background;.rotationEffect()via D2DSetTransform(D2D-renderable content only)
- Backend:
BackendWeb→ renders to DOM elements via JavaScriptKit - Compiler: requires open-source Swift toolchain (not Xcode's — Xcode strips the Wasm backend)
- Setup (macOS):
./configureinstalls swiftly + toolchain + Wasm SDK - Build:
swift build --swift-sdk swift-6.2.4-RELEASE_wasm - Package for browser:
swift package --swift-sdk swift-6.2.4-RELEASE_wasm js --product HelloWorld - Serve:
npx serve .build/plugins/PackageToJS/outputs/Package - Environment: single-threaded global (no TLS needed on Wasm)
- Debug builds are ~59MB; release builds will be significantly smaller
- DOM mapping: VStack →
flex-direction: column, HStack →row, ZStack → CSS grid, etc.
- Backend:
BackendAndroid→ Swift renders view tree to JSON → KotlinRenderHostbuilds Android Views - Requires Swift 6.3 dev snapshot toolchain (opt-in, not the repo default)
- Architecture design: Swift owns state/diff, Kotlin host owns UI (see Android Backend Design)
- Setup: see Android Setup Guide
- SwiftOpenUI core compiles for
aarch64-unknown-linux-android28via the official Swift Android SDK pthreadTLS works on Android viacanImport(Glibc)
- JSON bridge: Swift
AndroidRendererwalks the SwiftOpenUI view tree, producesRenderNodegraph, serializes to JSON (hand-written, no Foundation JSONSerialization) - JNI entry point:
@_cdecl("Java_com_example_swiftopenui_RenderBridge_nativeRenderApp")— single JNI call returns full JSON render tree - Kotlin host:
RenderHost.renderFromJSON()recursively maps JSON nodes to Android Views (TextView, Button, LinearLayout, FrameLayout, etc.) - Manual JNI: no swift-java bindings — JNI function table navigated manually (NewStringUTF at index 167, GetStringUTFChars at 169)
| SwiftOpenUI | Android View | Notes |
|---|---|---|
| Text | TextView | setTextColor(BLACK), SP units |
| Button | Button | isAllCaps=false |
| VStack | LinearLayout (VERTICAL) | alignment → Gravity, spacing → topMargin |
| HStack | LinearLayout (HORIZONTAL) | alignment → Gravity, spacing → leftMargin |
| ZStack | FrameLayout | Color children → MATCH_PARENT, others → CENTER |
| Spacer | Space | layout weight=1 in HStack/VStack |
| Divider | View (1dp, gray) | |
| Color | View (MATCH_PARENT) | rgba from props |
| .padding() | wrapper LinearLayout | setPadding in dp |
| .frame() | FrameLayout | explicit width/height in dp |
| .font() | setTextSize + setTypeface | recursive into child views |
| .foregroundColor() | setTextColor | recursive into child TextViews |
| .backgroundColor() | setBackgroundColor |
- Debug APK is ~77MB due to unstripped Swift runtime
.sofiles - Theme:
Theme.Material.Light.NoActionBarfor clean rendering fitsSystemWindows=trueon ScrollView to avoid status bar overlap- Intent extra
--es example "name"selects which example to render - Must force-stop app between intent launches (Android reuses existing Activity otherwise)
ViewBuilderno longer has a fixed child-count ceiling. It now uses incrementalbuildPartialBlockaccumulation into a flatViewList(MultiChildView) instead of relying only on fixed-aritybuildBlockoverloads.- This was a core framework issue, not a Win32-only issue. Any backend using SwiftOpenUI's custom
ViewBuildercould hit the old limit when examples or apps exceeded the supported tuple arity. macOS with real SwiftUI is unaffected because it does not use SwiftOpenUI rendering. - Backend note: renderers that recurse directly through
bodyfor primitive multi-child results must handleMultiChildView/ViewListexplicitly. GTK4 and Android already do this in their main dispatch; Win32 and Web needed explicit top-level fallbacks. #if os()inPackage.swiftchecks the host platform, not the cross-compile target- Example dependencies always include
SwiftOpenUI— source-level#if os(macOS)selects the import - Backend targets (GTK4, Win32) are still gated by
#if os()since they require platform-specific system libraries - Web backend and JavaScriptKit are always declared in the manifest to support cross-compilation from macOS to Wasm
The macOS launch fix is mostly about lifecycle discipline, and that lesson carries well to GTK4, Win32, and other native backends:
- Do not try to front a window before the platform has created and shown a real top-level window
- Keep the "bring app frontmost" helper passive; avoid taking ownership of the platform's primary app lifecycle if the toolkit already has one
- Separate these concerns in backend design:
- app activation / focus
- initial window show timing
- window sizing policy
- process termination on last-window-close
- Model size policy as independent fields:
- default width/height
- min width/height
- max width/height
- resizable enabled/disabled
- content-sized behavior where the platform supports it
- Expect platform policy limits:
- macOS may refuse focus changes until the app is eligible
- Linux window managers may ignore or reinterpret aggressive activation requests
- Windows generally allows more explicit foreground control, but only after a real HWND exists
- Add platform-native lifecycle logging before changing behavior; notification traces are usually more useful than speculation
Suggestion for Linux and Windows agents:
- Reuse the macOS mental model, not the exact API sequence
- Front only after the first mapped GTK window or shown HWND exists
- Prefer a small, local example-runner helper over embedding launch hacks in core rendering code
- Keep fixed-size and content-sized example windows as explicit backend policy, not accidental side effects