Glance Surfaces (widgets, lock screen, Dynamic Island)
A layout already declares what matters: its controls. Glance projects a handful of those live values onto every iOS at-a-glance surface — home-screen and lock-screen widgets, the Dynamic Island, StandBy — without any extra plumbing on your server. The same broadcast that moves a gauge in the app moves the gauge on the lock screen.
The mental model
One hero value + up to three secondary slots. That's the whole vocabulary, because that's what a 2-second glance can absorb. Everything the app renders big (gauges, rings, status lights) collapses to a tiny equivalent:
| Control type | Glance rendering |
|---|---|
gauge |
circular gauge with value |
progressRing |
capacity ring |
statusLight |
colored dot + state text |
slider, stepper, sparkline |
numeric readout (uses formatValue) |
toggle |
on/off glyph |
label, picker, segmentedControl |
text readout |
Zero-config default
Layouts with no glance block still get one: the app auto-picks the most
glanceable controls (gauges and rings first, then status lights, then any
numeric a server actively feeds via a listen sync). Add the block when you
want to choose.
The glance block
{
"name": "3D Printer",
"glance": {
"title": "Printer",
"icon": "printer.fill",
"tint": "#FF9F0A",
"hero": "nozzle_temp",
"slots": ["bed_temp", "print_progress", "printer_state"],
"liveActivity": true
},
"tabs": [ … ]
}
| Field | Meaning |
|---|---|
enabled |
false suppresses even the auto-derived glance. Default true. |
title |
Surface title. Defaults to headerTitle / name. |
icon |
SF Symbol glyph (Dynamic Island leading slot, widget header). |
tint |
Hex accent. Defaults to accentColor. |
hero |
Control id of the one value that represents the layout on the smallest surfaces (compact island, inline lock-screen text). |
slots |
Up to 3 control ids for secondary readouts. |
liveActivity |
Start a Live Activity while the layout is connected — the session lives in the Dynamic Island. Author-opt-in (default false); the island is prime real estate. |
controls |
System controls this layout offers — see below. |
System controls (glance.controls)
A layout can also offer buttons and toggles for Control Center, the lock-screen corner slots, and the Action button:
"glance": {
"controls": [
{ "id": "garage", "label": "Garage", "icon": "door.garage.closed",
"kind": "toggle", "control": "garage-toggle", "event": "garage_set",
"payload": { "room": "garage" } }
]
}
| Field | Meaning |
|---|---|
id |
Stable id within the layout. |
label / icon |
Gallery + control appearance (SF Symbol). |
kind |
"toggle" (stateful) or "button" (fire-and-forget, default). |
control |
Layout control id a toggle mirrors — its live value drives the Control Center state, and a press writes back to it. |
event |
MeshSocket event sent on press. |
payload |
Static object merged with {"value": <bool>} for toggles, plus source: "system-control" and control: <id>. |
Delivery: a press updates the control optimistically, queues the action in an app-group outbox, and nudges the app (Darwin notification). A running app sends it over the layout's socket immediately; a suspended app delivers on next foreground/connect. The planned relay HTTP bridge makes presses deliver even with the app dead.
Which layouts get surfaces? (the registration model)
Mirrors alert registration — the pinned deck is the registration set:
- Widgets + system controls: every pinned layout. The deck sweep publishes each pinned layout's glance + control declarations; the widget and Control Center galleries offer all of them, and the user places the instances they want (iOS never lets an app auto-place).
- Live Activity: the active layout only — it's a session surface tied to the one live socket. (Phase-2 push-to-start lets the relay open a session for any pinned layout remotely.)
How values flow
- While the app is open (foreground or briefly backgrounded): values update the Live Activity at ~1s cadence and the widget snapshot at the same pace; widget reloads are spaced ≥30s to respect WidgetKit's daily budget.
- When the app is suspended: widgets keep the last-known values and show an honest "x min ago" stamp plus a connection dot. This is deliberate — a glance surface that silently shows stale data as fresh is worse than no surface.
- Tapping any glance surface deep-links into the app and loads that layout
(
carter://glance?layout=<file>).
Live Activity notes
- Started when a
"liveActivity": truelayout connects; ended on disconnect or layout switch. The user can also disable Live Activities per-app in Settings. - The activity is requested with an ActivityKit push token, logged to the
connection console. Today updates are local (app-driven). The token is parked
in the app group for the phase-2 relay integration: the relay alert engine
already speaks APNs, so a future
carterkitcall will be able to drive the Dynamic Island while the app is dead. Rule declaration will mirror alerts (server-readable acknowledgement included, since the relay must read the watched field).
Widget picking
The home/lock-screen widget is configurable (long-press → Edit Widget): it lists every layout the app has published a glance for — effectively your pinned deck. An unconfigured widget shows the most recently active layout, so it works the moment it's added.
There is also a Control Center button (iOS 18): add "Open Layout" from the Control Center gallery — or bind it to the Action button — to jump straight into your most recent layout from anywhere, including the lock screen.