Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Run iOS Simulator Over SSH on a Remote Mac

The iOS Simulator is a macOS process, not a window. xcrun simctl — the command-line interface to CoreSimulator — can list and boot devices, install an app, launch by bundle ID, write a PNG of the framebuffer, and stream unified logs without a display server attached. From an SSH session on a remote Mac you can drive the whole headless loop and pipe output back to your laptop. This guide gives the commands, the real gotchas (UDIDs, simulator-vs-device builds, macOS lacking timeout), and how to pick a Figment plan that matches the Xcode toolchain you actually need.

How the iOS Simulator Works Without a Display

Simulator.app is a GUI wrapper around the CoreSimulator service and its per-runtime bundles. xcrun simctl talks to that service directly, so boot, install, launch, io, and spawn all work from a non-graphical session. A simulator booted this way runs real UIKit/SwiftUI code against the installed iOS runtime; what you lose is interactive touch, Instruments' GUI, and the visionOS simulator specifically — Apple's Xcode system requirements state that developing for visionOS requires a Mac with Apple silicon.

Two constraints matter before you SSH in:

  • Simulator binaries are not device IPAs. A .ipa from TestFlight or a Developer ID build is compiled against the iphoneos SDK and signed for a device profile. The simulator runs iphonesimulator-SDK .app bundles — the platform binary is different. Even on an Apple silicon host, you cannot simctl install a device IPA's Payload/*.app. Build a simulator variant (-sdk iphonesimulator) and transfer that .app directory.
  • Intel Xcode still builds simulator apps. Apple's porting guide says Intel Macs must use Xcode 26 or an earlier release; the Intel-supported Xcode line ships iOS simulator runtimes, so a Figment Virtual Intel Mac can compile and host simulator .app bundles for the matching iOS majors. Xcode 27, visionOS, and ARM-only toolchains need Apple silicon. Confirm your exact Xcode/iOS runtime pair against Apple's current release notes before committing.

List and Boot Simulators with xcrun simctl Over SSH

SSH into the physical M-series or i3 Mac you arranged in your quote. (The $29 Virtual Intel Mac is accessed through its browser-console desktop terminal — run the same simctl commands there; there is no inbound SSH on that VM today.) Throughout this guide, remote-mac is a placeholder for the hostname or user@host you were given; do not assume any Figment alias is pre-wired on your workstation — set it up in your own ~/.ssh/config if you want a short name.

# What's installed?
ssh remote-mac 'xcrun simctl list devices available'

Pick a device line and capture its real UDID — a 36-character value shaped like XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX. Storing it once avoids ambiguity when several device types share a display name:

UDID=$(ssh remote-mac "xcrun simctl list devices available -j" \
  | jq -r '[.devices[][] | select(.name=="iPhone 16")][0].udid')
echo "$UDID"

ssh remote-mac "xcrun simctl boot $UDID"
ssh remote-mac "xcrun simctl list devices | grep $UDID"

simctl boot returns non-zero if the device is already booted; check state first (simctl list devices | grep "$UDID") rather than blanket-swallowing the error.

Install a Simulator .app and Launch It Headlessly

Build locally or on the remote Mac with the simulator destination, then copy the resulting .app bundle up. Use a per-job mktemp -d so parallel runs never collide, and clean up with a trap tied to that run's directory — not a blanket rm -rf /tmp/anything.

# On your laptop (or the remote Mac), with the real scheme name from your project:
xcodebuild -scheme "$MY_SCHEME" \
  -configuration Debug \
  -sdk iphonesimulator \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath "$PWD/dd" \
  build

# Copy the simulator .app to a unique scratch directory on the remote Mac:
WORK=$(ssh remote-mac 'mktemp -d /tmp/sim.XXXXXXXX')
trap 'ssh remote-mac "rm -rf $WORK"' EXIT

scp -r "$PWD/dd/Build/Products/Debug-iphonesimulator/MyApp.app" \
  "remote-mac:$WORK/"

ssh remote-mac "xcrun simctl install $UDID $WORK/MyApp.app"
ssh remote-mac "xcrun simctl launch $UDID com.example.MyApp"

simctl launch prints the PID on success — pass it to lldb process attach --pid if you need to debug.

Stream Unified Logs and Capture Screenshots

xcrun simctl spawn <udid> log stream writes the unified log to stdout; filter locally with grep --line-buffered (without --line-buffered, block buffering can stall visible output for seconds).

ssh remote-mac "xcrun simctl spawn $UDID log stream --level debug" \
  | grep --line-buffered "MyApp"

macOS does not ship GNU timeout. For a bounded capture, either stop the stream manually with Ctrl-C, install gtimeout from Homebrew's coreutils, or background the command and kill it:

# Option A: install coreutils once on your laptop, then:
gtimeout 30 ssh remote-mac "xcrun simctl spawn $UDID log stream --level debug" \
  > "sim-$(date +%Y%m%d-%H%M%S).log" || true

# Option B: no extra install, portable:
ssh remote-mac "xcrun simctl spawn $UDID log stream --level debug" > sim.log &
LOG_PID=$!
sleep 30
kill "$LOG_PID" 2>/dev/null || true

Screenshots come straight from the framebuffer:

ssh remote-mac "xcrun simctl io $UDID screenshot $WORK/shot.png"
scp "remote-mac:$WORK/shot.png" ./shot.png

To wait past a launch animation, launch, sleep a few seconds, then shoot. When a run finishes, shut down only the UDIDs you own — never simctl shutdown all on a shared host, because other developers (or CI matrix jobs) may be using other devices:

ssh remote-mac "xcrun simctl shutdown $UDID"

Reader Tool: simctl-headless.sh

A compact helper you can source once per project. It wires the pieces above with the owned-UDID discipline, portable buffering, and job-scoped scratch dirs — no giant orchestration script, no hidden global state.

# simctl-headless.sh — source this file; export REMOTE=user@host in your shell.
: "${REMOTE:?Set REMOTE=user@host}"

sim_udid()     { ssh "$REMOTE" "xcrun simctl list devices available -j" \
                 | jq -r --arg n "$1" '[.devices[][] | select(.name==$n)][0].udid'; }
sim_work()     { ssh "$REMOTE" 'mktemp -d /tmp/sim.XXXXXXXX'; }
sim_boot()     { ssh "$REMOTE" "xcrun simctl boot $1" \
                 || ssh "$REMOTE" "xcrun simctl list devices | grep $1"; }
sim_install()  { ssh "$REMOTE" "xcrun simctl install $1 $2"; }      # $1=UDID $2=app
sim_launch()   { ssh "$REMOTE" "xcrun simctl launch $1 $2"; }       # $1=UDID $2=bundle
sim_shot()     { ssh "$REMOTE" "xcrun simctl io $1 screenshot $2/shot.png"; \
                 scp "$REMOTE:$2/shot.png" ./shot.png; }
sim_shutdown() { ssh "$REMOTE" "xcrun simctl shutdown $1"; }        # $1=UDID only
sim_cleanup()  { ssh "$REMOTE" "rm -rf $1"; }                       # $1=mktemp dir

Each function takes the UDID (and scratch dir where relevant) as an argument, so there is no shared "booted" state, no path is reused across jobs, and nothing targets devices you don't own. Treat it as a starting point — adapt names to your project before running against real builds.

Choose Between Headless simctl and VNC Xcode

TaskHeadless simctl over SSHXcode GUI via VNC
xcodebuild test in CIBest fitOverkill
Screenshot capturesimctl io screenshotSupported
Ad-hoc tap/gesture workLimited (simctl io help)Easier
Instruments profilingNot via simctlRequired
visionOS simulatorApple silicon host requiredApple silicon + GUI

For CI pipelines and automated acceptance tests, simctl over SSH is the simpler path. Switch to a VNC Xcode session only when you need Instruments, interactive debugging, or the visionOS simulator.

Which Figment Plan Fits This Workflow

  • Virtual Intel Mac — $29/mo (2 vCPU, 8 GiB RAM, 80 GiB disk). Runs the Intel-supported Xcode line (Apple directs Intel hosts to Xcode 26 or earlier). Access is the browser-console desktop terminal; inbound SSH is not currently implemented on this VM. No GPU acceleration or audio; ARM-only simulators and the visionOS simulator are not available here. Good for headless simulator work against the iOS runtimes that ship with an Intel-supported Xcode.
  • i3 Mac — $49/mo. Physical Intel Mac, request-based until supply is confirmed.
  • M-series Mac — from $95/mo. Required for Xcode 27, ARM-only execution, visionOS, or GPU-bound work. Exact chip, RAM, disk, OS image, and whether SSH is exposed are confirmed in your quote — $95 is an entry price, not a specified M4/16 GB configuration. Physical stock is request-based; include your timeline.
  • Managed Build Mac — $249/mo. One M-series Mac, CI setup, backups, monitoring, and 30 minutes of monthly support.
  • Managed Mac Team — $999/mo. Two M-series Macs, two CI runners, team setup, backups, monitoring, and one hour of monthly support.

A fair cost framing for this specific workflow: a solo developer who only needs headless simctl on an Intel-supported Xcode pays $29/mo on Figment's Virtual Intel Mac. CloudMac's listed M4/16 GB/256 GB monthly rate is $99.99 — about 3.4× more — for Apple silicon hardware you may not need for this task. Figment's M-series starts at $95/mo, but that entry price is not an M4/16 GB spec; the exact hardware is set in the quote. If a competitor's like-for-like plan is cheaper on confirmed specs, say so plainly and compare what each plan includes.

Confirm your Xcode/iOS runtime pair and request a Mac at /mac-hosting/start.


Related guides:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms