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-arm64produces a real arm64 iOS static library (platform 2, minos 13.0) and--target ios-arm64-simulatorproduces the simulator one.examples/facet_gallery_ioslinks 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 / Time ↔ NSDate, 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_appkitcreates 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
UIColorresolves against the current trait collection when asked;NSColorflattens against whatever appearance is current, which costs the AppKit backend a re-entrant saved-static wrapper around every configure. - No document view. A
UIScrollViewscrolls its own subviews. insertSubview:atIndex:exists, so slot ordering is one call.sizeThatFits:takes the bound, so a wrapping label needs nopreferredMaxLayoutWidthpre-step.isSecureTextEntryis a property, so there is no live reclass path at all —views::reclassexists 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.