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

flowchart LR server["your server<br/>broadcast / routed"] --> app["CAR-TER renderer<br/>controlValues"] app -->|"throttled ~1s"| store["app-group snapshot"] app -->|"throttled ~1s"| island["Live Activity<br/>(Dynamic Island)"] store --> widgets["widgets<br/>(home + lock screen)"]
  • 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": true layout 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 carterkit call 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.