Devices & Studio
Simulator Studio is a live, interactive mirror of a booted simulator — zero-copy frames, real input, and overlays that draw what the runtime graph knows on top of what the device is showing.
Bring-up is one click
Selecting a simulator drives a single de-duplicated bring-up sequence: boot it, launch the configured bundle if it is already installed, else resolve or build a simulator .app, install it, launch it, and open its dev client against Metro. You do not run five commands in the right order; you pick a device.
The whole sequence is a tracked process, so it reports itself in three places at once — a per-phase spinner in the device row (Booting, Installing app, Building app, Launching app, Opening app), the header status rail, and the notification centre. Device discovery stays live, so a simulator booted from Xcode appears without a refresh. Press a notification to read all of it: it opens in place with the whole message, which you can select or copy, and a link to its History entry when it has one.
The live frame
Frames come from the simulator’s framebuffer surface zero-copy — no simctl spawn, no PNG encode/decode round trip. The desktop makes one copy into a buffer and draws it in a GPU canvas. Capture is seeded by the device rather than polled, so an idle simulator costs nothing, and it downscales natively on the way out so memory stays bounded.
The frame is drawn on the UI thread with no React commit per frame, which is why it stays smooth while the rest of the app is doing work. Streams are pooled per device, so several simulators can stream at once — which is what the multi-device grid is: expand the panel (⌘ G) and every booted device becomes its own interactive cell, each with its own graph session keyed by the udid the app announces.
Driving it
The Interact toggle arms input. Everything then goes through the native input engine as real HID:
- Tap and live-streamed drag
- A drag streams down/move/up as it happens, with moves coalesced so a 60 Hz drag cannot flood the queue, and a guaranteed lift on cancel so the digitizer never sticks.
- Home and Back edge gestures
- A drag starting in an edge band is classified and tagged with the matching digitizer edge bit, so the simulator recognises it as a genuine system gesture while still following your finger.
- Wheel and trackpad scroll
- Scrolling over the canvas plants a virtual finger that follows every delta and lifts after a quiet period, re-planting near an edge so a long scroll does not trip the edge-gesture bands.
- Mac keyboard capture
- While interactive, your keystrokes route to the simulator — named keys and ⌘/⌃ combos by key code, printable characters through the injected agent’s caret insert (one ~3 ms call) with per-char HID as the fallback. ⌘ C and ⌘ V bridge the simulator pasteboard both ways. System shortcuts still reach macOS.
- Hardware buttons
- Home, Lock and volume, from the device panel.
- Keep software keyboard
- iOS hides the on-screen keyboard as soon as it believes a hardware keyboard is attached, and typing from your Mac makes it believe that. This toggle keeps the software keyboard on screen while you type. It needs the injected agent, is remembered per device, and is re-applied every time the app reconnects, because a relaunched app forgets it.
- Device skin
- Studio draws the device’s real bezel, read at runtime from the device chrome in your own Xcode install — nothing Apple-owned ships with Squiggle. Its side buttons are pressable: power locks the device and the volume buttons change the volume, while buttons the simulator has no equivalent for (Action, mute) are drawn but do nothing. A drawn skin is the fallback: for home-button devices, whose bezels the chrome files do not describe; when the frame no longer has the screen’s shape, as after a rotation; when the host is older than the feature; and when Real device skin is off. Device bezel turns the frame off altogether.
Simulator settings
Three menus in the device panel change the simulator itself. Everything they write goes through simctl, and the menus read the value back after every change. When the runtime cannot report a value, the menu shows — rather than guessing a default.
- System
- Dark appearance, Increase contrast, text size (a stepper through every Dynamic Type size from XS to AX5, plus a reset to the default), and the status bar: Clean sets 9:41, full signal and a charged battery, and Reset hands it back to the device.
- Location
- A fixed place from a short list of cities, one of the simulator’s moving scenarios — City Run, City Bicycle Ride, Freeway Drive — or clear the simulated location.
- Permissions
- Grant or revoke the app’s privacy permissions one at a time (location, photos, microphone, contacts, calendar, reminders, motion, media library, Siri), or grant or reset all of them. Granting all may restart the app. The simulator has no way to read a permission back, so a tick means “granted from here”, not the device’s current state, and the menu says so. With no app to grant to, only Reset all is available.
Compact mode
⌘ ⇧ M, or the compact button in the title bar, shrinks the same window down to the device alone. The window turns transparent, so the device floats over your desktop with one bar above it, and the live frame keeps streaming through the switch because the device panel is resized rather than rebuilt. Pressing it again restores the window to where and how large it was.
- The title row
- The device menu, the connection pill, Customize bar, Keep on top (floats the window above other windows while compact), and exit.
- The tools row
- Any of the device panel’s menus — Interaction, Hardware, System, Capture, Overlays, Graphs, Location, Permissions — either whole or as single buttons pulled out of them, such as Home, Lock, Screenshot or Record screen, or Inspect component to pick from the device. Customize bar picks and reorders them. The row has twelve slots: a button takes one, a whole menu two, a stepper three. Out of the box it holds Inspect component, Screenshot, Record screen, Record session, Dark appearance, Keep software keyboard, and the Overlays and Graphs menus.
- Graphs
- Up to four small graphs of the last minute, read from the same performance store as the Performance screen. A tile turns red and says why when its metric breaches a rule: UI or JS FPS under 50; process, JS-thread or UI-thread CPU over 90%; event-loop lag over 100 ms; memory or footprint up more than 20% in ten seconds, or the JS heap up more than 30%. Hovering a tile opens the Performance screen’s chart of that metric.
The device grid is off while compact, since the window is sized for one device. Entering compact from the grid shows the focused device.
Overlay modes
The overlay cycle draws what the runtime knows on top of what the device is showing. Cycle through: none → renders → all → accessibility → heatmap.
- No overlay
- A clean, unannotated device — for demos and for screenshots.
- Renders
- Component bounds tinted by how often each has re-rendered, so a component that is repainting on every keystroke is visible as a colour, not as a number in a list.
- All bounds
- Every node the graph resolved a rect for.
- AX tree
- The device’s live native accessibility tree — bounds and labels, mapped onto the frame. When the app is injected this is push-driven: it repaints only when the tree actually changes, and is silent while idle. Uninjected it falls back to self-paced polling of an external sweep. This is also the plane your flow selectors fall through to, so an element missing here is an element a flow cannot name.
- Heatmap
- Every touch as a density blob plus every swipe as a vector — see Interaction analytics.
- Native overlay
- A second, click-through window parked over the real Simulator.app window, drawing the same rects with Core Animation. For when you want to work in the actual simulator and still see the annotations. It hides itself when another window is stacked over the simulator.
Runs are bounded windows
A run is a capture window, not a connection. It gets its own id and its own row in the store, so one continuous app session can hold several discrete runs — start → interact → stop → inspect. A run auto-starts on connect and on relaunch; ● Start run mints a fresh one, ■ Stop run flushes and freezes it on screen, and Reset run wipes the current one in place while continuing to capture. Past runs are selectable from a picker across the analytics, performance and product screens, and deletable individually.
This is the same shape a CLI invocation gets for free: one squiggle flow is one run.
One project at a time
Every store plane — flows, recordings, perf.db, history, screenshots, agent memory — derives from a single active project root, chosen explicitly through a real folder picker and switchable from the title bar or the home banner. Switching remounts every project-scoped screen, so no data survives the change. It is the same .squiggle the CLI resolves by walking up from your working directory, which is what lets a flow you ran in a terminal show up in Studio’s history.
Flow Studio
Flow Studio turns the same canvas into an authoring tool, driving the same engine the CLI does — so a flow authored here runs identically in CI.
Record
It captures both desktop-mediated taps and taps you make directly in the Simulator window, resolving each one against the live accessibility tree and the graph into a semantic step. Generic container labels and decorative roles are skipped. Composite labels are split: iOS joins a nav bar’s children into one accessibility label — "Forward, Animations" — and recording the whole string would produce a selector that matches nothing, so the recorder takes the segment that is actually the title.
Edit
A visual step list — add, remove, reorder, edit — with a grouped step palette (Interact, Wait/Assert, Control flow, Device), so a whole flow can be hand-built rather than only recorded. Delete, revert and save live per step as well as per flow.
Organise
Flows group into folders — real subdirectories of .squiggle/flows/, so the structure is in the repository rather than in a database. Sort by name, last replayed or date added, and move a flow between folders by dragging it.
Replay, with eight toggles
The replay log streams each step with a spinner on the live one, then freezes as the last run with its verdict and timings. Four of the checkboxes under Options are the delivery knobs described in Speed & delivery, under the same names as their CLI flags — In-process delivery (on by default), Disable animations, Direct scroll and Fast settle. The other four change what a run does rather than how fast it does it: Auto-scroll scrolls an off-screen target into view instead of refusing the tap; Auto-correct selectors runs the flow’s prerequisite as a pre-phase, so the app starts from a known state and the correction is reported outside steps; Capture network installs the pass-through NSURLProtocol; and Continue on failure reports every step instead of stopping at the first red one.
Doctor and Crawl, in place
Both reuse the live connection rather than opening a second server: Doctor runs the connectivity and graph-health check, Crawl runs the bounded autonomous crawler and loads the flow it discovered. Their reports share the replay panel.
The relay, so the terminal still works
Whenever an app is connected the host automatically re-serves that session to CLI consumers on a second port. There is no toggle. So squiggle flow runs against an app Studio is already holding rather than fighting it for the socket — the CLI dials the relay when the primary port is taken.
While a CLI is attached, a CLI flow attached chip appears and the desktop’s own Replay button is disabled, because two drivers on one app is a race, not a feature.
The rest of the app
- Home
- Devices, the connection status, the active project banner, and whatever is currently running. The place a session starts.
- History
- Every run this project has produced — flows, benches, crawls, recordings — with its outcome and duration, and a re-run that replays the exact invocation recorded with it. Shared with
squiggle history, because both read one JSONL file. - Settings ⌘ ,
- Appearance, sound, the licence, and Install CLI in PATH…, which symlinks the bundled
squigglebinary into~/.local/bin.