- Role
- A driver that gives AI agents the iOS Simulator
- Problem
- Every agent IDE that wants a simulator pane rebuilds a piece of CoreSimulator in private. simctl can't send a touch, and XCUITest needs a runner.
- Constraint
- Real HID input and live frames with nothing installed in the simulator, no daemon, and no crash that can take the host app down.
- Outcome
- One Rust core with three small helpers: 60 fps frames over shared memory, touch through the simulator's own digitizer service, and accessibility snapshots — open, and drivable from Node, Rust, or a CLI.
The gap every agent IDE keeps re-solving
Watch an agent IDE drive iOS and you'll notice each one built its own glass. Claude's app has an iOS pane. Codex had a picture-in-picture simulator. Every one of them reverse-engineered the same frameworks to get pixels in and touches out, and none of that work is reusable.
The tools underneath don't cover it. simctl can boot, install, and launch, but it cannot send a tap. XCUITest drives real input, but it needs an xcodebuild runner built and signed before the first tap lands, and it still can't reach the system gestures an agent needs — the home indicator, the app switcher. Meta's idb gets closer, but its HID surface carries a single contact and no edge flag, so swipes from the screen's edge die quietly.
So simdriver is the piece I kept wishing existed: one open driver a host embeds — an Electron app, a Rust window, a CLI — and the whole simulator just works inside it. The loop was already proven at 60 fps inside a prototype app I built a week earlier; simdriver's v1 was deliberately boring. Package what worked, change nothing.
A driver, not a screen recorder
The design decision underneath everything: pixels, not an overlay. The driver hands you frames — about 60 a second — and you draw them wherever you like. An Electron pane, a Rust window, a terminal cell. The host keeps its UI; the simulator is just content.
Three tiny Objective-C helpers carry the private-API surface, one process each, supervised by the Rust core with restart and backoff:
- Frames. The simulator's own GPU surface, read through CoreSimulator's private screen objects and packed straight into a three-slot shared-memory ring under a seqlock. The newest frame wins; a slow consumer skips, never queues. This is not a screen capture — the framebuffer is the device's own surface.
- Touch. The simulator's digitizer service (
dtuhidd) over XPC — the same path Simulator.app uses, so input lands as real HID events, not synthesized clicks. When another client takes the digitizer, simdriver steps aside and reports it, then reclaims on your next touch; frames never stop.
- Accessibility. A snapshot of the guest's element tree through the private translation framework, with nothing installed inside the simulator.
The gesture detail cost the most and is worth naming. iOS decides a swipe is a system gesture from an edge flag that must ride on every contact sample, not just the first. idb's transport can't send it, which is why edge swipes die there. In simdriver it's one field on the wire:
Gesture interpretation — which zone a touch started in, pinch versus pan — lives in the core as a pure state machine, so every host behaves identically. The private-header shapes the helpers compile against come from Meta's idb, vendored with a NOTICE: everyone loves open source, and attribution is how it stays open.
The same driver already renders a live simulator inside a terminal pane — frames over shared memory, touch included — which is the strongest proof the pixels-not-overlay bet was right.
What holds, what does not
Working today:
- 60 fps frames with p99 gaps around 23 ms on the dev machine; the integration tests fail the build under 55 fps or 34 ms
- about 9 ms median from tap to the first frame that shows it
- real HID touch, hardware buttons, and keyboard, with system gestures working
- accessibility snapshots with re-validated refs, batched steps, and load-free recording
- nothing installed in the simulator, no daemon, and no private framework loaded into the host
Still rough:
- the helpers compile against private frameworks, so every Xcode release is a compatibility check — that table lives in the README and lags first, by design
- no biometrics, no device orientation, and two fingers is the limit
- non-ASCII
fill breaks on the current Xcode's pasteboard path
key() and text() need "Connect Hardware Keyboard" enabled
The honest summary: the pixels and the touch are solved problems now. The maintenance contract with Apple's private frameworks is the part that never finishes — and packaging it once, in the open, is how the cost gets shared instead of paid by every agent IDE again.