Build an Interactive iOS Simulator Broadcast over Tailscale
I wanted to see and operate a native iOS app from my phone while the Mac running Xcode was elsewhere. Simulator Webcast makes the Mac’s iOS Simulator viewable and touch-controllable from a browser over Tailscale. It is not a physical iPhone, TestFlight, or a web rewrite of the app.
The current package has a local Mac control page for choosing a Simulator, starting/stopping broadcast, and seeing boot progress. Its separate tailnet-only preview carries video, taps, swipes, Home, and Run app. The repository README is the canonical reference; this is the end-to-end setup.
1. Prepare the Mac#
The Mac hosts both Xcode’s Simulator and the webcast server; the phone or other viewing computer only needs a browser and Tailscale. Install the full Xcode app (Command Line Tools alone are insufficient), open it once to complete setup, and install an iOS Simulator runtime under Xcode β Settings β Platforms. In Terminal, confirm that Xcode and at least one available iOS device are listed:
xcodebuild -version
xcrun simctl list devices available
Install Homebrew if needed, then Meta’s idb tools. idb-cli supplies the CLI and Python package; idb-companion communicates with Simulator. idb provides the preferred MJPEG stream and native taps/swipes. Video can fall back to slower simctl screenshots, but touch still requires idb. Verify both commands exist:
brew tap facebook/fb
brew install idb-cli idb-companion
idb --version
command -v idb_companion
Boot a Simulator in Xcode or Simulator. Build and run your iOS app on that Simulator. Find its bundle ID under Xcode β Target β General β Bundle Identifier. For that device, record the logical screen width and height in points, not screenshot pixels; you can log UIScreen.main.bounds.size from the running app or consult the device model’s logical-point dimensions. The installer asks for the initial device’s dimensions; switching devices recalculates them. If you only want to view Simulator Home, a placeholder bundle ID such as com.example.MyApp is fine. Run app requires the configured bundle ID to be installed on whichever Simulator is selected; switching devices does not change the ID.
Sign into the Mac’s graphical desktop account and keep it logged in and awake while broadcasting. The per-user LaunchAgent can restart the server within that session, but cannot log you into macOS after reboot. Locking an already logged-in session differs from logging out; sleep, logout, and shutdown interrupt the preview. After reboot, log back in and boot the selected Simulator.
2. Connect both devices with Tailscale#
Install Tailscale for macOS on the Mac hosting Simulator. Open it, sign in, connect, and allow the requested macOS VPN/network-extension permission. If you have no tailnet, signing up creates one. Xcode is needed only on this Mac.
Install Tailscale on every phone or computer that will watch the preview. Sign into the same tailnet and connect its VPN. On iPhone/iPad, accept the VPN configuration prompt. Tailscale must be connected on both ends when viewing.
On the Mac, get its private Tailscale IPv4:
tailscale ip -4If the CLI is not on your
PATH, try/Applications/Tailscale.app/Contents/MacOS/Tailscale ip -4or read the address in the Tailscale app. Use the Mac’s address, not the viewer’s or a public IP. The installer can detect it automatically.Confirm both devices are online in the Tailscale admin console. The tailnet policy must let the viewer reach the Mac on TCP 8765. A new personal tailnet’s default policy permits device-to-device connections; if you use custom grants/ACLs, allow this connection. Port 8766 is for the Mac’s localhost-only control page and need not be allowed remotely.
You do not need Tailscale Funnel/Serve, a public DNS name, a VPS, or a router port-forward. The preview is HTTP on a private tailnet address. Anyone permitted by your tailnet policy to reach that port can view/control the Simulator while broadcast is on, so keep the policy appropriately scoped.
3. Install Simulator Webcast#
On the Mac, run this one-liner as the logged-in Mac user. It fetches the latest release archive, unpacks it temporarily, and starts the interactive installer:
bash -c "$(curl -fsSL https://raw.githubusercontent.com/PrajeetShrestha/simulator-webcast/main/install-latest.sh)"
It requires curl, tar, python3, and GitHub access, but no gh or jq. You can inspect the bootstrap script first. It removes only its temporary download after installation. For a non-interactive agent install, append the installer flags after _:
bash -c "$(curl -fsSL https://raw.githubusercontent.com/PrajeetShrestha/simulator-webcast/main/install-latest.sh)" _ --udid SIMULATOR_UDID --bundle-id com.example.MyApp --point-width WIDTH --point-height HEIGHT --tailscale-ip MAC_TAILSCALE_IP
Run as the logged-in Mac user, not with sudo. The installer lists available iOS Simulators and asks for the initial device, app bundle ID, logical width/height in points, and Mac Tailscale IPv4. It verifies idb, installs a per-user LaunchAgent, and prints the local control and tailnet preview URLs. python3 install.py --help shows flags for a non-interactive install, including --tailscale-ip.
The control server starts in the logged-in session, but broadcast starts off. Previous server and LaunchAgent files are backed up when reinstalling.
4. Let an existing AI agent build and broadcast your app#
An existing coding agent can do the native-app work; it needs shell access to the Mac running Xcode, not a connection to the webcast’s video URL. If the agent runs on that Mac, use its local shell. If it runs on a VPS or another computer, join that host to the same Tailscale tailnet, enable System Settings β General β Sharing β Remote Login on the Mac, and add the agent host’s SSH public key for the Mac user. Test from the agent host:
ssh MAC_USER@MAC_TAILSCALE_IP 'xcodebuild -version && xcrun simctl list devices available'
Keep the Mac’s graphical account logged in and awake. A successful SSH login cannot substitute for that GUI session after reboot. The agent can then create a SwiftUI/Xcode project (or use your existing one), build for iphonesimulator, and install/launch it on the exact Simulator selected in Webcast. For an existing project, the core commands on the Mac are:
xcrun simctl boot SIMULATOR_UDID # skip if already Booted
xcrun simctl bootstatus SIMULATOR_UDID -b
xcodebuild -project MyApp.xcodeproj -scheme MyApp \
-sdk iphonesimulator -destination 'platform=iOS Simulator,id=SIMULATOR_UDID' \
-derivedDataPath /tmp/myapp-simulator-build CODE_SIGNING_ALLOWED=NO build
xcrun simctl install SIMULATOR_UDID \
/tmp/myapp-simulator-build/Build/Products/Debug-iphonesimulator/MyApp.app
xcrun simctl launch SIMULATOR_UDID com.example.MyApp
Adjust the scheme, .app path, and bundle ID to match the project; use -workspace for a workspace. The agent can use the non-interactive one-liner in Install Simulator Webcast above in that same Mac user account, without sudo.
Use the chosen Simulator’s logical point dimensions for WIDTH and HEIGHT. If Webcast was installed with another bundle ID, rerun the installer; rebuilding the app does not alter its configured Run app target. Broadcast initially stays off. Open the Mac-only control page at http://127.0.0.1:8766/ and turn it on, or let the agent call its local endpoint on the Mac:
curl -fsS -X POST -H 'Origin: http://127.0.0.1:8766' http://127.0.0.1:8766/enable
curl -fsS http://127.0.0.1:8766/status
curl -fsS http://MAC_TAILSCALE_IP:8765/health
Check that the selected Simulator is booted, enabled and ready are true, and frames increase; then open http://MAC_TAILSCALE_IP:8765/ from your tailnet-connected phone/computer and test a tap. The agent reaches the Mac by SSH for build and local control; the viewer reaches the separate private preview port. Turn off Broadcast from the control page when finished (or POST to /disable with the same Origin header).
A useful agent prompt is: βOn my Tailscale-connected Mac, create a native SwiftUI app with [your requirements] and bundle ID com.example.MyApp. Build it for the selected iOS Simulator, install and launch it using simctl, install/configure Simulator Webcast with that UDID and bundle ID, enable broadcast locally, verify /status and /health, and return the private preview URL. Do not publish ports or use physical-device signing.β Replace the example ID and provide the actual device and app requirements. No agent-specific webcast plugin or API key is required.
5. Start, view, and control#
- On the Mac, open
http://127.0.0.1:8766/. Keep the selected Simulator booted. Use the device picker if needed; it shows boot/switch progress and the real Booted/Shutdown state. When a switch succeeds, the previously selected Simulator shuts down; unrelated booted Simulators are left alone. - Turn Broadcast on. Copy the preview URL shown by the control page.
- On the viewer, with Tailscale connected, open
http://MAC_TAILSCALE_IP:8765/in a browser. Tap or swipe the image to interact. Home sends the Simulator’s actual Home button regardless of the running app. Run app launches the configured bundle ID, which must be installed on the selected device. Screenshot opens the latest frame. - Turn Broadcast off from the Mac control page when finished. The stream and input endpoints are unavailable while off.
Switching devices while broadcasting retargets the video and input. If the new Simulator fails to boot, the old selection is retained. The Mac’s control page is deliberately not accessible from your phone; use the phone for the preview.
6. Verify and troubleshoot#
On the Mac:
curl http://127.0.0.1:8766/status
curl http://MAC_TAILSCALE_IP:8765/health
launchctl print "gui/$(id -u)/com.codekunda.simulator-webcast"
tail -n 50 "$HOME/Library/Application Support/SimulatorWebcast/server.err"
/status shows the selected Simulator, switch phase/result, broadcast state, and frame freshness. /health should report "ready": true with a low age_ms and an increasing frames count. Also test an actual tap and swipe from the viewing device: video and input are separate paths.
- Viewer times out: Check that Broadcast is on, both devices are connected to the same tailnet, the Mac is awake/logged in, and your tailnet policy allows viewer β Mac TCP 8765. Confirm the Mac’s current
tailscale ip -4matches the installed address; if it changed, rerun the installer with the new--tailscale-ip. - Page loads but video is frozen: Check
/health, the selected Simulator’s Booted state, and the server log. idb MJPEG is preferred; the server falls back to slowersimctlscreenshots if necessary. - Video works but touch does not: Confirm
idb_companionis installed, refresh the browser page, and try a visible button. Check the server log for idb errors. - Run app fails: Install the configured bundle ID on the selected Simulator. Home works independently of that app.
- After reboot: Log into the Mac, reconnect Tailscale if necessary, and boot the selected Simulator. The LaunchAgent cannot log in or boot it for you.
Under the hood, idb streams compressed Simulator JPEG frames to a Python HTTP server and carries native taps/swipes back to Simulator. A persistent idb input connection avoids starting a CLI process for every gesture. In one setup this produced roughly 7.5 fps across the tailnet, versus 1β2 fps with screenshot polling; your route and device will vary. The listener binds only to the configured Tailscale IPv4. Do not expose its unauthenticated HTTP port to the public internet.