ADE Apple Development

SkillMedia

Use this skill when you need to see an iOS or SwiftUI change actually running on a simulator, finding or creating the lane's device, booting and streaming it, driving it by element query, screenshotting, recording, capturing proof, or rendering a SwiftUI preview, via `ade apple`.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the ADE Apple Development skill

What this skill tells your AI

The instructions your AI receives, as published by arul28/ade in apps/desktop/resources/agent-skills/ade-apple/SKILL.md and read by ahel’s review.

This skill is for iOS and SwiftUI apps. For a macOS app, a dev Electron app or a web page, the ade-computer-use skill picks the right surface.

Drive the lane's Apple simulator with "$ADE_CLI_PATH" apple <command>. $ADE_CLI_PATH is the CLI of the ADE that launched you and already targets its brain, so your calls and the desktop's Apple Development tool share one session. A bare ade goes through PATH and can reach a different ADE.

The pixels come from a vendored Swift helper (ade-sim-helper) reading the device framebuffer. There is no Screen Recording grant, no Simulator window to keep open, and no backend to choose. If you find advice about idb, window capture, live-start or a --backend flag, it is describing a version of this feature that no longer exists.

Four rules before your first command

  • Always call "$ADE_CLI_PATH", never a bare ade. A bare ade can be an older install or another ADE's CLI. That shows up as "Unknown command 'apple'", "Domain 'apple' is unavailable", or a runtime version mismatch. Do not add --socket: "$ADE_CLI_PATH" already names the right one.
  • Never open or script the Simulator app. No open -a Simulator, no AppleScript or System Events. ADE drives the device directly; the Simulator window is not needed and adds nothing.
  • Never capture proof with xcrun simctl io … screenshot or recordVideo. apple screenshot and apple record-start / record-stop file proof to the lane automatically. See below for why the owner matters.
  • Check each step before you report it. A tap, swipe, button or type that returns ok only means ADE sent the input. It does not mean the app did what you wanted. After each step that matters, confirm it: apple foreground (which app is in front), apple assert-visible or apple wait-for-element (what is on screen), or apple snapshot. Report only what you confirmed. If a step failed, say which step, and do not describe the result you meant to get. Before record-stop, confirm the final state, so the video ends on it.

Common tasks

Use these directly; you do not need --help for them. A="$ADE_CLI_PATH".

TaskCommand
Open an installed app"$A" apple relaunch --bundle-id com.apple.mobilesafari --text
Open a web page or deep link"$A" apple open-url "https://www.google.com" --text
Tap a control by its label"$A" apple tap-element --label "Address" --text
Type, then press Return"$A" apple type "reddit" --submit --text
Press one key"$A" apple key return --text (also tab)
Wait for something to appear"$A" apple wait-for-element --label "Cancel" --timeout-ms 8000 --text
Which app is in front"$A" apple foreground --text
What is on screen"$A" apple snapshot --text
Go home / app switcher"$A" apple button home --text / "$A" apple button app-switcher --text
Close an app (no gesture)"$A" apple terminate --bundle-id com.apple.mobilesafari --text
Show the device to the user"$A" apple show --text (tools pane) or --floating
  • Right after relaunch, foreground can still say null for a moment. Wait for an element of that app instead of checking at once.
  • A simulator signed in to an Apple Account can show an "Apple Account Verification" alert over every app. Dismiss it with tap-element --label "Not Now", then continue.

Start here: ask what you can do

"$ADE_CLI_PATH" apple status --text

status is the gate and the map.

  • supported: false means the runtime is not a Mac. Stop and say so.
  • capabilities lists every action you may call on this device, by name. Read it instead of guessing verbs or reading source.
  • device tells you whether this lane already owns a simulator, its origin (created or the older clone for ADE's own, attached for the user's), and its laneId.
  • tools.helper (present, path, version) is the check when every device call fails at once.

"Start the app on a simulator" is one command

"$ADE_CLI_PATH" apple launch --follow --open-drawer --text

launch does the whole chain: find or make the lane's device, boot it, resolve the target, build it with xcodebuild, install it, start it, and claim the drawer session. Add --open-drawer when a human asked to watch. --follow announces the wait up front for a cold build.

So you do not run xcodebuild by hand to put an app on a screen, and you do not need to know the scheme. Run "$ADE_CLI_PATH" apple apps --text first only when you must choose between several targets, then pass --target <id>.

Use --no-build when the app is already installed and you only want it in front. Use relaunch or terminate for an app that is already there.

Use this lane's own device

Use this lane's own device. ade apple start (or device-create) gives the lane its own; do it without asking.

start with no arguments does this for you: when the lane has no device it makes a new, empty one from the newest installed runtime, boots it, and streams it. Other simulators in apple devices belong to someone else: another lane, an xcodebuild test run, or the user.

  • You may make the lane's ONE device without asking. You may not make a second one, and you never make a simulator with xcrun simctl create or simctl clone — ADE cannot see or delete a device made that way.
  • You may attach an installed simulator only when the user names it (device-attach --simulator <name> or start --udid <udid>). ADE refuses a simulator another lane holds, with APPLE_DEVICE_NOT_LANE_OWNED.
  • --device <udid> on any other command must be this lane's device. With no device yet, the command tells you to run ade apple start.

A busy device is not a blocker, and it is not a reason to ask a human. Make the lane's own with start. It takes seconds (see "The lane's device").

Tests run on the lane's device

"$ADE_CLI_PATH" apple test --scheme <scheme> --text
"$ADE_CLI_PATH" apple test --target <id> --only ADETests/SyncTests --text

apple test runs xcodebuild test on this lane's device, with parallel testing off and DerivedData in the lane's cache (.ade/cache/ios-simulator/DerivedData), which ADE deletes with the lane. One test run goes at a time on the Mac (APPLE_TEST_RUN_BUSY: wait, then run again). A failing test comes back as passed: false with the failures and the log path; it is not an error.

Do not run xcodebuild test by hand against a simulator you made. If you must run xcodebuild yourself, pass -derivedDataPath .ade/cache/ios-simulator/DerivedData from the lane worktree, never /tmp or ~/Library/Developer/Xcode/DerivedData.

This is worth saying plainly because the failure looks reasonable from the inside: you find a booted simulator, the guard tells you another chat owns it, and stopping to ask seems polite. It is not. It blocks the work for no reason and hands the human a decision they should never have been given.

And when the request was for simulator proof, simulator proof is what you owe. A passing unit test is not a substitute for a screen. If you cannot reach the screen, say exactly what stopped you rather than offering evidence of a different kind and calling it done.

The lane's device

One simulator per lane. ADE creates one on first ask, or binds one you already have. ADE never downloads a runtime. With none installed you get APPLE_NO_INSTALLED_SIMULATORS — tell the user to open Xcode ▸ Settings ▸ Components.

Two facts that decide what you should do:

  • A runtime is the iOS image. It is the large download, one per iOS version, and ADE will not fetch one.
  • A device is an instance made from a runtime. Creating one copies no iOS, so it is cheap. A new lane device is empty: it copies nothing from any other simulator and grows only with what the lane installs. A device that is Shutdown is installed and ready — it needs a boot, which takes seconds.

So "no device free" is almost never true: one runtime serves any number of devices, and a new one is a folder of app data, not another copy of iOS. A new device has no sign-ins, so the app starts at its first-run state.

"$ADE_CLI_PATH" apple device-list --installed --text   # what a picker shows
"$ADE_CLI_PATH" apple device-list --runtimes --text    # installed runtimes and models
"$ADE_CLI_PATH" apple device-list --text               # the one this lane owns ($ADE_LANE_ID)
"$ADE_CLI_PATH" apple start --text                     # make one if the lane has none, boot, stream
"$ADE_CLI_PATH" apple start --device-type "iPad Air 11-inch (M4)" --text   # a specific model
"$ADE_CLI_PATH" apple stop --text                      # power the device OFF
  • start is the one-step bring-up: make a device when the lane has none, boot it if it is off, wait for the boot, then stream. Prefer it. start --udid works only for the device this lane already holds.
  • stop powers the device off. shutdown only ends this chat's session and leaves the simulator running — the two are not the same, and a verb named stop that left a booted device behind was a real bug.
  • device-create and device-attach never boot. device-delete removes the lane's ADE device and refuses an attached device unless --force, which only detaches. Like stop, it is refused while another chat is driving the device.
  • ADE deletes the lane's own device, with all its data and the lane's build cache, when the lane is archived or deleted. An attached device is powered off and loses only the apps ADE installed. ADE never deletes a simulator it did not create.
  • An ADE device that nothing uses for 30 minutes is powered off. start boots it again in seconds.
  • Deleting an installed simulator from the list is the user's call, made in the desktop picker. ADE refuses it from an agent; do not try.

Drive the app

Name elements, not pixels.

"$ADE_CLI_PATH" apple snapshot --text
"$ADE_CLI_PATH" apple tap-element --label "Sign in" --text
"$ADE_CLI_PATH" apple fill-element --identifier email-field --value ada@example.com --text
"$ADE_CLI_PATH" apple wait-for-element --label Welcome --timeout-ms 8000 --text
"$ADE_CLI_PATH" apple assert-visible --label Welcome --text
  • Run snapshot first. Query with --ref, --identifier, --label, --text-match, --role and --index.
  • A ref names its own tier. id: and component: survive a re-render; pos: survives nothing, so ask for an accessibility identifier instead.
  • wait-for-element replaces a sleep. Add --gone to wait for a disappearance.
  • Fall back to coordinates only when no query matches: tap, drag, swipe, scroll, select.
  • type and key act on the focused field, so tap the field first.
  • foreground reports which app the device has in front, read from the device rather than from what ADE last launched.
  • Hardware buttons: button home (also lock, volume-up, volume-down, siri, app-switcher). shake is refused with APPLE_BUTTON_UNSUPPORTED, because neither the helper nor simctl has it. app-switcher is Simulator's own App Switcher command, two home presses 150 ms apart; do not press home twice yourself, the gap between two calls is too long.
  • Orientation: rotate landscape-left (also portrait, portrait-upside-down, landscape-right). Read the answer. rotate turns the device and then reads the screen, so applied: true means the framebuffer was seen on the requested axis — not that a request was sent. iOS always accepts the device orientation and then lets the foreground app decide, so applied: false with reason: APPLE_ROTATE_NOT_ADOPTED means that app kept its own orientation. The Home Screen and Settings are portrait-only on an iPhone, and no iPhone supports portrait upside down — put the app you are testing in front before you rotate, and do not treat a refusal as a broken simulator. verification: already-on-axis means the screen was on that axis before you asked; turning within one axis leaves the pixel size unchanged, so the exact side is not confirmed.

Agent launches stay in the background; apple show brings the device up at any time.

Close an app the way a person does

Bottom-edge swipes do not work: the helper sends them as ordinary touches, so iOS never sees the home-indicator gesture, and a swipe from the bottom edge opens nothing. Use the App Switcher instead.

"$ADE_CLI_PATH" apple button app-switcher --text
"$ADE_CLI_PATH" apple screenshot --out switcher.png --text   # check it opened
"$ADE_CLI_PATH" apple swipe 201 480 201 80 --duration-ms 150 --text
"$ADE_CLI_PATH" apple button home --text
  • Coordinates are points. Read the screen size from apple stream-status (pointWidth × pointHeight); the example is an iPhone 16 Pro (402 × 874).
  • The front app's card sits in the middle of the switcher. Swipe it up from about (W/2, 0.55 H) to (W/2, 0.09 H), fast: 150 ms. A slow drag only lifts the card.
  • Take a screenshot after app-switcher. If the switcher is not showing, do not keep swiping: say so, and use apple terminate --bundle-id <id> to stop the app without showing it (Safari is com.apple.mobilesafari).
  • While a recording runs, every failed try is in the video. Check the screen after each step instead of repeating a gesture.

Show the device to the user

When the user asks to see the device ("open the sim drawer", "show me"):

"$ADE_CLI_PATH" apple show --text              # the Apple tool in the tools pane
"$ADE_CLI_PATH" apple show --floating --text   # the floating player over the chat
"$ADE_CLI_PATH" ui show proof --text           # this chat's proof drawer

It targets your own chat and reports what really happened. A shell with no ADE chat identity (ADE_CHAT_SESSION_ID unset, e.g. an OpenCode agent shell) cannot use apple show or ui show; ask the user to open the tool.

  • shown — it is on screen now.
  • held — a desktop window has this project open but the user cannot see it yet (your chat is not in front, or the window is hidden). It opens when the user goes to your chat. Say so.
  • no_desktop (exit 1) — no desktop window is open for this chat, so nothing was shown. Tell the user; do not claim you opened it.

A desktop on another machine that has your chat open answers too. You do not need show just to be seen working: while you drive the device and the Apple tool is not open, the desktop floats the device over your chat by itself, unless the user closed that player (then only apple show or the user brings it back).

Proof is automatic, and only on this path

You do not pin anything. Both of these file themselves in the proof drawer and return a proofArtifactId:

  • every recording, the moment it stops, whoever started it;
  • every screenshot you take.
"$ADE_CLI_PATH" apple screenshot --out shot.png --text
"$ADE_CLI_PATH" apple frame --out shot.png --text
"$ADE_CLI_PATH" apple proof-bundle --caption "Settings row renders" --text

Capture through these commands, not through xcrun simctl io screenshot. The difference is not the picture, it is the owner. An ade apple capture is filed against the lane and the asking chat, so it appears in the drawer the human is looking at. A picture you take with simctl and then hand to ade proof attach is filed against whatever ADE can work out about your shell, and a shell with no chat session — every OpenCode agent has one, because a single opencode serve is shared across chats — used to produce a record owned by nothing, invisible to everyone, while every command reported success. That now fails loudly instead of lying, which is better and still not proof.

If you must attach a file from elsewhere, run ade proof attach from inside the lane worktree so ADE can place it, and read the last line: it names the lane and the chat the artifact landed on. lane none / chat none is a failure, not a detail.

  • screenshot round-trips simctl and works with no stream running.
  • frame grabs one decoded frame from the live stream and fails with APPLE_STREAM_NOT_RUNNING when there is none. Prefer it while streaming.
  • proof-bundle files the same single drawer row as screenshot, and also writes elements.json, the log and metadata.json next to screen.png in a directory under the build root. Use it when a reviewer wants the whole context; the row it returns is the picture.

Recording, and what starts one

"$ADE_CLI_PATH" apple record-start --overlays on --label "signup" --text
"$ADE_CLI_PATH" apple record-stop --keep --text
"$ADE_CLI_PATH" apple record-list --text
"$ADE_CLI_PATH" apple record-delete --id <id> --text
  1. Your input starts a recording; the user's never does. Any input from the CLI, an agent action or a semantic action counts as evidence and starts one automatically, tagged auto. Input from the desktop pane is stamped as the user's and starts nothing.

  2. It stops at the end of your turn, or after five minutes, whichever is first. record-start during an auto recording converts it to manual with no gap. A manual recording stops itself after five minutes of real time (stopReason: "cap"), or after two minutes with no input ("idle"), and files as proof; --max-seconds <n> sets a shorter limit. Still call record-stop when you are done.

    When it stops, the recording becomes a demo: still time is cut, waits play faster, tap rings are drawn, and the file ends under 10 MB. The camera does not zoom on a phone; pass record-start --zoom only when the user asks for zoom. Mark each step with ade proof step "<what happens next>": a caption in the video and a chapter in ADE's player. record-stop reports durationMs (the demo), wallDurationMs (real time) and idleCutMs. Pass --plain only when the user asks for the recording as it was recorded.

  3. It files itself as proof on stop. There is no auto-delete; recordings accumulate until the user clears them.

  4. You may delete a recording you own. Anything else is APPLE_RECORDING_PINNED.

  5. A second chat gets APPLE_OWNED_BY_OTHER_SESSION with the owning chat, lane and age.

  6. If recording fails, say so. Never attach an older recording or a file you did not just record.

Overlays, meaning tap rings and typed-text badges, land in the saved file only and never on a live viewer. Secure text is never badged.

Live view

"$ADE_CLI_PATH" apple stream-start --fps 60 --text
"$ADE_CLI_PATH" apple stream-start --scale-factor 0.5 --bitrate-kbps 2500 --text
"$ADE_CLI_PATH" apple stream-status --text
"$ADE_CLI_PATH" apple stream-stop --text

stream-status reports the shape and never the address or the token. A status of running: true means the capture is alive, not that a picture is arriving.

Device settings, log, previews

"$ADE_CLI_PATH" apple appearance dark --text
"$ADE_CLI_PATH" apple accessibility reduce-motion on --text
"$ADE_CLI_PATH" apple location 37.7749 -122.4194 --text
"$ADE_CLI_PATH" apple permission grant photos --bundle-id <id> --text
"$ADE_CLI_PATH" apple push --bundle-id <id> --title Hi --body "You have mail" --text
"$ADE_CLI_PATH" apple log-start --bundle-id <id> --text
"$ADE_CLI_PATH" apple preview-current --text

Also content-size, status-bar, open-url, relaunch, terminate, uninstall, app-state, log, log-stop, preview-status, previews, preview-match, preview-ensure, preview-render.

What the user sees in the desktop

Worth knowing, because it changes what is running under you.

  • The tool is Apple Development, one pane inside the Work tools pane.
  • Closing its tab powers the device off, behind a confirmation when the device is booted. Minimising the tools pane leaves it running and shows a floating preview.
  • The rail carries Home, Rotate, Inspect, Screenshot, Record, the 3D/Flat view toggle, Tools and More. The drawer has four groups: Device, App, Capture and Preview Lab.

Ownership

One chat owns a simulator session at a time. A second launch fails with APPLE_OWNED_BY_OTHER_SESSION.

  • Ownership releases when the owning chat is deleted or archived.
  • The guard is cooperative: shutdown --force, launch --force and claim --ignore-ownership get through. Ask before you evict another chat.
  • claim --lane <lane-id> attaches a running session to a lane.

Gotchas

  • APPLE_NO_INSTALLED_SIMULATORS — no installed runtime. Point at Xcode ▸ Settings ▸ Components. Never try to download one.
  • APPLE_HELPER_UNAVAILABLE — the helper binary is missing from this install. status → tools.helper.present is the check.
  • APPLE_STREAM_NOT_RUNNING — frame needs a live stream; use screenshot.
  • APPLE_DEVICE_OFF — the lane's device is powered off, and watching never boots it. apple start (or stream-start) powers it on.
  • APPLE_ROTATE_NOT_ADOPTED — the device turned and the app on screen did not. Launch a landscape-capable app first; it is not a fault to report.
  • APPLE_ROTATE_UNMEASURABLE — the screen could not be read, so the rotation is unconfirmed either way. Check the device is still booted.
  • APPLE_BUTTON_UNSUPPORTED — that button has no helper or simctl equivalent. Today: shake.
  • APPLE_DEVICE_ATTACHED_NOT_DELETABLE — device-delete without --force on an attached simulator. --force detaches; it does not delete.
  • APPLE_DEVICE_EXISTS — this lane already owns a device. Use it, or remove it first.
  • APPLE_DEVICE_NOT_LANE_OWNED — you named a simulator this lane does not hold, or another lane holds it. Do not look for a way around it. Run ade apple start to get the lane's own, or attach it first if the user named it.
  • APPLE_RUNTIME_NOT_INSTALLED — the runtime or model you named is not on this Mac. The message lists what is; ADE never downloads one.
  • APPLE_TEST_RUN_BUSY — another apple test is running. Wait and run again.
  • IOS_SIMULATOR_TARGET_ROOT_MISMATCH — re-run "$ADE_CLI_PATH" apple apps --text.
  • IOS_SIMULATOR_NO_BUILDABLE_TARGET — pass --target-id or --bundle-id only when you deliberately want the installed app.
  • screenshot --out and frame --out must land inside the build root.
  • --text reads two ways. A bare --text is ADE's output mode; --text-match <value> is the element query's substring match.

Signals

GitHub stars
110
Forks
13
Last commit
Sep 2026
Advanced
Item type
skill
Key
ade-apple
Source
github.com/arul28/ade