C+
Packages · View as Markdown

facet_uikit

facet's UIKit backend — the iOS counterpart of facet_appkit.

Status: runs in the simulator; not on a device. cpc build --target ios-arm64 produces a real arm64 iOS static library (platform 2, minos 13.0) and --target ios-arm64-simulator produces the simulator one. examples/facet_gallery_ios links against it and runs: layout, measurement, view ownership, the run-loop tick and target/action delivery have all been observed working. What is unvalidated is FEEL — a drag, a fling, momentum, the exact moment a swipe opens — because those cannot be asserted from a screenshot and nobody has held the phone.

Prop parity with facet_appkit: 305 of 318 (95%). The thirteen-prop difference is MANIFEST.md §1 in full — a date or time picker's font band (twelve verbs UIDatePicker has no property for) and a window's chrome style. Nothing is missing for want of writing.

That number is a measurement, not a claim — tools/parity.py walks facet's contract modules and asks which bits each backend names:

python3 vendor/facet_uikit/tools/parity.py      # from the repo root

Anything it prints under UIKIT MISSING must appear in MANIFEST.md §1. It also reports the other direction, which is how the four props facet_uikit honours and facet_appkit does not were found.

Test

vendor/facet_uikit/tools/run_ios_tests.sh      # builds, installs, runs, reports

cpc test cannot run this package — it builds a host binary and macOS has no UIKit. The checks live in src/selftest.cplus and run as an iOS binary (tests/) under simctl. The script exits non-zero when anything fails.

Build

cd vendor/facet_uikit
cpc build --target ios-arm64             # device: target/ios-arm64/debug/libfacet_uikit.a
cpc build --target ios-arm64-simulator   # simulator
cpc check --target ios-arm64             # sema + borrowck only, faster

Xcode owns the final link — an iOS target stops at object emission by design (plans/plan.backends.md, rung 1). Add the .a and a bridging call to facet_uikit::install() from the app's main.

What it is

facet owns the description tree (facet::Node) and all layout (the shared flex_layout engine). This package fills the same five-verb Renderer seam facet_appkit fills, and no wider surface:

File Role
facet_uikit.cplus install() — the whole registration surface
views.cplus create / apply / insert / remove / view_release, the backing rule, measurement, and this package's tests
controls.cplus one body per kind
paint.cplus the shared band — colour, brush, shadow, clip, radius, transform, animate_*
geometry.cplus the layout pass and the frame walk
input.cplus target/action and tap recognisers → facet handlers, and the radio group
scheduler.cplus the CFRunLoop tick, run_on_main, after
text_input.cplus the field / editor / search band, style runs, the length limit
recycler.cplus list, table, tree on UITableView; collection, carousel on UICollectionView
drawing.cplus the canvas replay — facet's recorded display list into a CGContext
swipe.cplus swipe-to-reveal, driven by a pan so reveal_threshold means something
web.cplus web and hybrid_web over WKWebView
dates.cplus facet's Date / TimeNSDate, through NSDateComponents
window.cplus the app delegate, the window, and the first tick

Where UIKit is SHORTER than AppKit

Worth reading before porting anything else from facet_appkit, because these are the places where copying that code would add machinery for no reason:

  • No flip. A UIView is top-left origin natively. facet_appkit creates a flipped subclass for every node and says so in three files.
  • Always layer-backed. No setWantsLayer:, so every paint verb is one call shorter and every clear is unconditional.
  • No appearance dance. A UIColor resolves against the current trait collection when asked; NSColor flattens against whatever appearance is current, which costs the AppKit backend a re-entrant saved-static wrapper around every configure.
  • No document view. A UIScrollView scrolls its own subviews.
  • insertSubview:atIndex: exists, so slot ordering is one call.
  • sizeThatFits: takes the bound, so a wrapping label needs no preferredMaxLayoutWidth pre-step.
  • isSecureTextEntry is a property, so there is no live reclass path at all — views::reclass exists in the AppKit backend for that single prop.
  • One synthesized class, not one per kind. Target/action takes a separate target object, so no view has its class moved.

Three things are genuinely richer here: SF Symbols (systemImageNamed: is the whole implementation), Dynamic Type, and the keyboard band — which facet declares on every input and macOS can do nothing with.

What is not here

MANIFEST.md is the authority, and it keeps two lists strictly apart:

  • decided absent — iOS has no such thing (window buttons, the menu bar, a draggable split divider). Finished; silent at runtime.
  • not yet built — iOS has an answer and this pass did not write it (the list/table/collection/tree recycling tier, web, canvas, swipe actions). Each renders its children through a plain backing view and warns once on stderr when mounted, so the debt is audible.

The largest single piece of work left is the recycling tier: facet_appkit's recycler.cplus is 2,900 lines and has no counterpart here.

Wired into facet

import "facet_runtime/runtime" resolves to facet_runtime/src/runtime_ios.cplus on an iOS target — the same filename override that picks runtime_macos.cplus on a Mac, keyed off target::active_platform(). facet_runtime's manifest carries [ios.dependencies], so facet_runtime's own build on iOS compiles this package, exactly as it compiles facet_appkit on macOS. The consequence is the one facet's manifest already states for the Mac: a backend that does not build turns facet red.

facet/src/agent_ios.cplus shadows agent.cplus the same way, because that module is written against agent_appkit and there is no agent_uikit. It keeps the surface compiling and refuses loudly.

cd vendor/facet && cpc check --target ios-arm64     # facet + this backend

Application lifetime and navigation

App::run registers the initial window with UIKit and enters the OS-owned loop. It does not return on iOS. Window content and navigation are retained by that window's session; there are no standalone component/screen host APIs.

Content commands use the window's navigator, changing its whole content or a named slot. An explicit window-opening request never becomes a content push. Additional iPad windows require an app that opts into multiple scenes. Shared screens can be used by separate desktop and mobile app compositions.

Save persistent state on backgrounding rather than relying on on_quit or process-exit cleanup. Closing an additional scene has its own window teardown; process termination does not guarantee a final callback.

The current shared navigation engine supports screen-driven Back. The older UIKit controller stack's swipe behavior does not validate interactive gestures or scene restoration through the redesigned navigator. See the navigation guide for the current validation limits and API.