Skip to content

The Apple host

Generated from docs/hosts/apple.md when the site was built at 7aa23e2.

hosts/apple is niso’s SwiftUI host for every Apple platform: iOS and iPadOS, macOS, visionOS, tvOS and watchOS, with CarPlay through templates. It links crates/niso-app (the C API with QuickJS, or Porffor) for the platform’s Rust target, applies the render stream to a tree of SwiftUI views, and reports the user’s input back through the C API.

One host, not one per platform (see DECISIONS.md, Targets): the renderer (NodeView.swift, RenderNode.swift), the engine (Engine.swift), media (Media.swift) and page loading (PageLoader.swift) are shared, and what differs lives in small shims:

Shim What differs
Sources/Platform/Platform.swift the toolkit’s color, font and image types (UIKit, AppKit, watchOS’s UIKit subset); screen scale; whether the app is in the background; the storage directory; measuring stock views (UIHostingController, NSHostingController, ImageRenderer); the display frame clock (CADisplayLink, the display link of the Mac display the window is on, a 60 Hz timer on watchOS); the stock button’s and text field’s metrics; the text styles and their sizes
Sources/Platform/Input.swift how input reaches the page (below)
Sources/Platform/Controls.swift the native controls as each platform has them: text fields and their selection, the stock and filled button styles, radio groups’ pickers, sliders, and Liquid Glass (iOS, iPadOS, macOS, tvOS and watchOS 26), so the renderer has no platform checks of its own
Sources/Appearance.swift the semantic colors each platform has, resolved in the color scheme and with Increase Contrast
Shared/TextScale.swift Dynamic Type’s categories where the platform has them
Platforms/<platform>.plist each platform’s Info.plist keys

A new platform is a branch in each shim and a plist, not a fork.

Terminal window
hosts/apple/build.sh [--platform P] [EXAMPLE | --page DIR | --page FILE [--root DIR] | --app LIB] [--name NAME] [--bundle-id ID] [--icon PNG] [--out DIR] [--debug] [--no-launch] [[--] APP ARGS...]

P is ios-sim (the default), ios, macos, tvos-sim, tvos, visionos-sim, visionos, watchos-sim or watchos. The script builds niso-app for the platform’s Rust target (on an Apple silicon Mac the macOS one is the host’s own build), compiles the host with swiftc against the platform’s SDK and leaves an ad-hoc signed NAME.app: target/ios for ios-sim, target/apple/P for the rest. It launches the app on a Mac, or on the simulator NISO_SIMULATOR names; device builds need provisioning to install. Options come in any order. NISO_LIBDIR points it at a prebuilt libniso_app.a and its headers instead of running cargo, so a machine short of disk can build one Rust target at a time. Otherwise cargo builds for rustup’s aarch64-apple-* target (darwin, ios, ios-sim, tvos, tvos-sim, visionos, visionos-sim, watchos, watchos-sim), which the script adds if it is missing, and the headers come from that build’s own build scripts, into the output directory.

An app runs one of:

  • a JS bundle in its resources (NisoScript in Info.plist), the examples;
  • an HTML page (--page DIR, or --page FILE alone or in --root DIR): the root is copied into the app’s resources (as an APFS clone) and served at the app’s origin, https://<bundle id>.app.niso/, by a niso loader (niso_dom_set_loader), and its index.html loads from there with niso_dom_load_html. ES modules, stylesheets, fonts, images, wasm and workers (which load through the window’s loader) all come from the root; a path that leaves it, by .. or by a symbolic link, is not served, and other origins go to the network. The core types a response by its file’s extension (net.rs), which doesn’t know fonts and images yet. In a --debug build, -niso-page FILE -niso-root DIR runs a page from anywhere on disk, as niso-iced’s --page/--root do;
  • an app library in any language (--app LIB, see niso_app.h).

tests/apps/vscode/make-mac-app.sh bundles VS Code for the web this way as “VS Code (niso).app”.

Checked with Xcode 27.2 on an Apple silicon Mac: every platform builds for its simulator and its device, and counter runs where a runtime is installed (taps driven with -niso-tap, a Mac’s clicks with -niso-events).

Platform Builds Runs Input Native controls Limits
iOS simulator, device iOS 27 simulator: counter, radio taps; hover from a trackpad or mouse Liquid Glass buttons, switches, sliders, pickers, segmented controls, text fields with selection a device build needs provisioning to install
iPadOS the iOS app (iPhone and iPad) iPadOS 27 simulator (iPad Air 13-inch): counter, in a resizable window as iOS as iOS as iOS
macOS native SwiftUI on AppKit (not Catalyst) counter, VS Code for the web (--page) every mouse button, hover, wheel and trackpad scrolling, keys push buttons, switches, sliders, pickers, TextField and TextEditor with selection; one resizable window VS Code shows Ctrl shortcuts: navigator.platform is empty in the core; the core fires no resize event when the window resizes (the page reflows) and no contextmenu on a right click
visionOS simulator, device visionOS 27.2 simulator: counter gaze and pinch as a pointer; hover visionOS’s own buttons on the window’s glass (no Liquid Glass there) no ornaments: a page has no way to declare one yet
tvOS simulator, device tvOS 27.2 simulator: counter the Siri Remote as keys (arrows, Enter, Escape, MediaPlayPause) buttons (a filled one keeps its CSS box and lifts with focus), pickers, text fields; a progress bar for a slider spatial navigation and the focus ring wait for the tv-input core
watchOS simulator, device watchOS 27.2 simulator: counter taps; the Digital Crown scrolls Liquid Glass buttons, toggles, pickers (as navigation links), system text input no UIKit views, fixed colors on black, one text size
CarPlay in the iOS app verified in the app only (tests/carplay/check.sh): DeviceHub’s CarPlay window was unavailable on Xcode 27.2 CarPlay’s own CPListTemplate, CPNowPlayingTemplate audio apps’ templates only; not yet seen on a CarPlay screen
  • iOS and iPadOS: a tap is niso_dom_tap; a trackpad or mouse pointer hovering the page moves the mouse pointer (niso_dom_pointer); a live image (a canvas) takes touches as pointers.
  • macOS: AppKit’s events as they reach the window (a local event monitor), before its views take them, so native controls still get theirs: every mouse button and hover is the mouse pointer, the wheel and trackpad are wheel events (pixels for a trackpad, lines for a wheel) that scroll the native scroll views unless the page cancels them, and keys are key events with UI Events key and code values, which a focused native field then types unless the page cancels them. The system’s Command shortcuts (Quit, Hide, Minimize, Close, the next window) work whatever the page does with them, and while an input method composes the page gets Process keys (isComposing needs a flag niso_dom_key doesn’t take yet). A control’s tracking loop swallows its mouse up, so the host reports it once the loop ends. Keys the page doesn’t cancel go on to AppKit only when a native view (a field’s editor, a focused control) is first responder; the window would beep at them. tests/mac-input/check.sh plays input into the window (-niso-events) and checks what the page saw (tests/check.sh --mac includes it; it skips without a logged-in session).
  • visionOS: looking at a point and pinching is a pointer down and up there (a touch, so the lift is a click); a connected pointer hovers.
  • tvOS: the Siri Remote is keys, as a TV browser’s remote is: the touch surface’s directions are the arrow keys, select is Enter, Menu is Escape and play/pause MediaPlayPause, with the page as the focused view. This follows the tv-input branch’s design (docs/tv.md there): once its core lands, the host declares a directional input profile (niso_dom_set_input), spatial navigation moves the DOM’s focus, and the host draws the focus ring from niso_dom_focus_ring.
  • watchOS: taps, and the Digital Crown scrolls the native scroll views; text fields open the system’s text input.

Text fields are the platform’s own, and report edits with the selection they leave (niso_dom_text_edit) where SwiftUI gives it (iOS, iPadOS, macOS, visionOS), and show the selection the page sets. A Mac’s multi-line field is a TextEditor, where Return types a newline as in a textarea.

Every platform gets the color scheme, Bold Text’s weights, the text styles and the semantic colors as --apple-system-* (see DECISIONS.md, Elements). A Mac adds its own colors under WebKit’s macOS names (control-accent, selected-content-background, text-background, …), with tint the user’s accent color, and the high-contrast appearance with Increase Contrast. Reduce Transparency makes glass and materials the host draws solid. Pages can’t yet query prefers-contrast, prefers-reduced-motion or prefers-reduced-transparency: the core has no setter for them.

CarPlay draws only templates, allowed per app category; this host is an audio app (the com.apple.developer.carplay-audio entitlement). The simulator build embeds it in the binary, as Xcode does, but leaves it out of the ad-hoc signature: the simulator refuses to launch an unprovisioned app whose signature claims it. A page declares what CarPlay shows as surfaces, as it does widgets:

  • carplay.list is a CPListTemplate: its first text outside a button is the section’s title and each <button> a row (its first text the row’s text, its second the detail); choosing a row activates the button in the page (niso_dom_activate) and shows Now Playing.
  • Now Playing is CPNowPlayingTemplate, which shows the page’s media session as Media.swift gives it to the system.

examples/radio declares its streams as a carplay.list.

CarPlay is verified in the app only. Xcode 27 replaced Simulator.app with DeviceHub, whose CarPlay window was unavailable (greyed out) for iPhone simulators on iOS 27.0 and 27.2 with Xcode 27.2, so no CarPlay screen has shown these templates yet. -niso-carplay-check stands in for one, as a launch by CarPlay alone: the page belongs to the app (Engine.main), not to its window, and CarPlay’s scene starts it when it connects, so the phone needn’t show it. CarPlayCheck builds the list section from the page’s surface with the code the CarPlay scene uses, logs it, chooses the first station that isn’t playing, and passes when the page has activated it (its row has the detail), Now Playing was pushed, and the system’s Now Playing shows the station playing. tests/carplay/check.sh runs it on the iOS simulator (tests/check.sh --ios includes it); the station streams from the network.

The app has one window on every platform, as it has one page: UIApplicationSupportsMultipleScenes is false on iOS, iPadOS and visionOS, so an iPad opens no second window to fight the first over the page’s viewport, focus and safe area. CarPlay’s scene shouldn’t need the key: it is a scene of another role (CPTemplateApplicationSceneSessionRoleApplication), not a second window, and CarPlay apps are reported to work with it false (Apple’s WWDC22 manifest sets it true). That too waits for a CarPlay screen: the simulator’s CarPlay display can be turned on, but its scene connects only once the app is chosen on that screen, which nothing here can do.