
You can run Xcode UI tests on a rented Mac without owning hardware. The pattern is small: create a disposable iOS Simulator, run xcodebuild test against it, let it write a .xcresult bundle, and pull attachments out with xcrun xcresulttool. Every command below runs from the Mac's own shell — either the browser-console desktop on the Figment virtual Intel macOS plan, or the terminal on a physical M-series Mac you rent for ARM execution.
Match the plan to what the suite actually needs. If the tests depend on a current iOS Simulator runtime, pick the M-series Mac from $95/month — the exact chip, RAM, and disk are confirmed in the quote, and physical plans collect requests until supply is ready. If you only need Intel-compatible builds, signing, or an outbound CI agent against an older Xcode line, the $29 virtual Intel macOS plan runs Xcode 26 or earlier (per Apple's porting guide: developer.apple.com/documentation/apple-silicon/porting-your-macos-apps-to-apple-silicon); access is through a browser console rather than inbound SSH, and the VM has no GPU acceleration or audio, so Simulator UI frame performance on that plan is not something we have measured for you. Request a configuration.
XCUIApplication is the proxy your test uses to launch, activate, and terminate your app inside a Simulator. The class documents init(), init(bundleIdentifier:), launch(), activate(), terminate(), and the launchArguments / launchEnvironment properties at developer.apple.com/documentation/xcuiautomation/xcuiapplication. A setup that boots a known-clean app looks like:
let app = XCUIApplication()
app.launchArguments = ["-UITesting"]
app.launchEnvironment = ["BASE_URL": "https://staging.example.com"]
app.launch()
The shell side needs the same discipline. Do not run xcrun simctl shutdown all or xcrun simctl erase all on a shared machine — those commands destroy every simulator on the host, including devices other jobs or teammates are using. Create a disposable device named for this run, confirm its UDID in the command's output (or in xcrun simctl list devices), and delete only that one when the test finishes. Replace the scheme, project, and runtime with values from the project you actually ship:
set -euo pipefail
TS=$(date +%Y%m%d_%H%M%S)
SIM_NAME="uitests-${TS}-$$"
# Confirm the runtime and device type IDs on your machine:
# xcrun simctl list runtimes
# xcrun simctl list devicetypes
RUNTIME="com.apple.CoreSimulator.SimRuntime.iOS-18-0"
DEVTYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-16"
UDID=$(xcrun simctl create "$SIM_NAME" "$DEVTYPE" "$RUNTIME")
trap 'xcrun simctl shutdown "$UDID" >/dev/null 2>&1 || true; \
xcrun simctl erase "$UDID" >/dev/null 2>&1 || true; \
xcrun simctl delete "$UDID" >/dev/null 2>&1 || true' EXIT
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination "platform=iOS Simulator,id=${UDID}" \
-only-testing:MyAppUITests/OnboardingUITests \
-resultBundlePath "./UITests_${TS}.xcresult"
The trap ... EXIT block is the whole point: it tears down only the device this run created, even when xcodebuild crashes mid-suite. Check xcodebuild -help and xcrun xcresulttool help on the machine before committing the pipeline — the flags shift between Xcode releases, and matching the tool output you actually have beats copying commands from any article.
-resultBundlePath produces a directory containing the structured action record, coverage data, and attachments (screenshots, screen recordings, XCTAttachment payloads). There are two realistic ways to look at it:
open UITests_<timestamp>.xcresult loads the graphical test report with attachments inline. Fastest when you are already logged into the Mac.xcresulttool exposes an export attachments subcommand that writes files to --output-path. Add --only-failures to limit the dump to failing tests, or --test-id to scope to one test. The older get --format json shape is deprecated and requires --legacy; the move to get test-results summary and the new export shape is tracked in Apple's developer forum thread at developer.apple.com/forums/thread/763888.The script below — ui-test-collect.sh — runs the isolated Simulator, exports attachments on failure, and writes a summary JSON for CI dashboards:
#!/usr/bin/env bash
# ui-test-collect.sh <Scheme> [SimulatorDeviceType] [RuntimeID]
# Runs a UI suite on a disposable Simulator, then exports attachments.
set -euo pipefail
SCHEME="${1:?Usage: $0 <Scheme> [DeviceType] [RuntimeID]}"
DEVTYPE="${2:-com.apple.CoreSimulator.SimDeviceType.iPhone-16}"
RUNTIME="${3:-com.apple.CoreSimulator.SimRuntime.iOS-18-0}"
TS=$(date +%Y%m%d_%H%M%S)
WORK_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/figment-ui-test.XXXXXX")
BUNDLE="$WORK_ROOT/UITests.xcresult"
OUT="$WORK_ROOT/attachments"
mkdir -p "$OUT"
UDID=$(xcrun simctl create "uitests-${TS}-$$" "$DEVTYPE" "$RUNTIME")
cleanup() {
xcrun simctl shutdown "$UDID" >/dev/null 2>&1 || true
xcrun simctl erase "$UDID" >/dev/null 2>&1 || true
xcrun simctl delete "$UDID" >/dev/null 2>&1 || true
}
trap cleanup EXIT
XCODE_STATUS=0
xcodebuild test \
-scheme "$SCHEME" \
-destination "platform=iOS Simulator,id=${UDID}" \
-resultBundlePath "$BUNDLE" || XCODE_STATUS=$?
# Modern attachment export (Xcode 16+). Confirm on your machine with:
# xcrun xcresulttool help export attachments
xcrun xcresulttool export attachments \
--path "$BUNDLE" \
--output-path "$OUT" \
--only-failures || true
# Structured summary for CI dashboards.
xcrun xcresulttool get test-results summary \
--path "$BUNDLE" > "$OUT/summary.json" 2>/dev/null || true
echo "Result bundle : $BUNDLE"
echo "Attachments : $OUT"
exit "$XCODE_STATUS"
The script preserves xcodebuild's exit code so a failing suite still fails CI; counting a crashed run as a passing benchmark is how flaky suites survive for months. The || true on the export calls is deliberate — you still want the bundle preserved if xcresulttool returns an unexpected status on an older toolchain. Drop --only-failures to export every attachment, or run open "$BUNDLE" on the Mac for the Xcode report. Transport the bundle off the machine with your CI's own upload step; keep signing secrets in the CI's secret manager rather than in shell history.
Flaky UI suites on remote Macs rarely come from the rental — they come from timing assumptions that only held on a foregrounded laptop. Four fixes cover most real cases:
element.waitForExistence(timeout: 5) only waits for the element to appear in the accessibility hierarchy; it does not guarantee the element is on-screen, enabled, or tappable. Before .tap(), also assert element.isHittable, or wait on an NSPredicate(format: "exists == true AND isHittable == true") expectation.sleep().tearDown, dismiss the keyboard or send Return explicitly rather than relying on the next test to ignore it.If you run the CI agent on the Mac itself, review GitHub's self-hosted runner hardening guide: only run trusted code on the runner, keep signing material in the CI's secret manager, and do not dump the shell environment to a plaintext file.
For custom checkpoints, XCTAttachment captures a screenshot at any step and lets you keep it on green runs:
func testCheckoutFlow() {
// drive the cart...
let shot = app.screenshot()
let attachment = XCTAttachment(screenshot: shot)
attachment.name = "cart-before-payment"
attachment.lifetime = .keepAlways // default .deleteOnSuccess prunes on pass
add(attachment)
}
| Job to be done | Figment plan | Monthly |
|---|---|---|
| ARM Simulator UI tests on a current Xcode | M-series Mac (physical, request-based) | from $95 |
| UI runner delivered working: CI setup, backups, monitoring, 30 min support | Managed Build Mac | $249 |
| Team runner: two M-series Macs, two CI runners, 1 hour monthly support | Managed Mac Team | $999 |
| Intel-compatible builds, signing, or an outbound CI agent (no GPU, no audio) | Virtual Intel macOS | $29 |
Managed Build Mac at $249 is the shortcut when you want the UI-test runner handed to you working rather than scripting it yourself; $95 is the entry price for an M-series Mac, with the specific chip, RAM, and disk confirmed in the quote. Physical plans collect requests until supply is confirmed, and inbound SSH is not currently implemented on the $29 VM — access there is the browser-console desktop, which runs xcodebuild, simctl, and xcresulttool from the terminal exactly as shown above. Start a configuration request.
Related reading: