---
title: "Run iOS UI checks in CI without a device farm | Mobster"
description: "Run iOS UI tests on every pull request with a Mac mini, simulators and one plugged-in iPhone: the xcodebuild flags, a GitHub Actions workflow and the limits."
canonical: "https://mobster.dev/blog/ios-ui-checks-ci-no-device-farm"
last-updated: "2026-10-17"
---

Run iOS UI tests on every pull request with a Mac mini, simulators and one plugged-in iPhone: the xcodebuild flags, a GitHub Actions workflow and the limits.

# Run iOS UI checks in CI without a device farm

Run iOS UI tests on every pull request with a Mac mini, simulators and one plugged-in iPhone: the xcodebuild flags, a GitHub Actions workflow and the limits.

The Mobster team 17 October 2026 6 min read

On this page

1. [Step 1: make Xcode fast on CI](#step-1-make-xcode-fast-on-ci)
2. [Step 2: pick where it runs](#step-2-pick-where-it-runs)
3. [Step 3: add Mobster’s checks to the pull request](#step-3-add-mobsters-checks-to-the-pull-request)
4. [Step 4: add a real iPhone and a nightly run](#step-4-add-a-real-iphone-and-a-nightly-run)
5. [What this doesn’t do](#what-this-doesnt-do)
6. [Questions](#questions)
7. [Sources](#sources)

A device farm rents you hundreds of phones. Most teams need three things: UI tests on every pull request, across a couple of screen sizes and iOS versions, and a real phone for what a simulator can’t show. A Mac mini does that for the price of the Mac. This guide builds the pipeline in four steps, and ends with what it can’t do and when a farm is the better tool.

## Step 1: make Xcode fast on CI

Whatever tool runs your tests, these `xcodebuild` habits apply. Build once, then run the tests without rebuilding:

```
xcodebuild build-for-testing -scheme YourApp \  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \  -derivedDataPath buildxcodebuild test-without-building -scheme YourApp \  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \  -derivedDataPath build -resultBundlePath build/result.xcresult \  -parallel-testing-enabled YES -parallel-testing-worker-count 2
```

- `-parallel-testing-enabled YES` clones the simulator and runs test classes side by side. Size the worker count to the Mac’s memory: one booted iPhone 17 Pro simulator with a small SwiftUI app used 0.7 GB in Mobster’s measurement (28 Sep 2026).
- `-resultBundlePath` keeps the `.xcresult`, with the screenshots and the log of every failure. Upload it as an artifact.
- Run only what a pull request touched with `-only-testing:YourAppUITests/CheckoutTests`.

## Step 2: pick where it runs

| Runner | Simulators | Real iPhone | You pay |
|---|---|---|---|
| A GitHub-hosted macOS runner | Yes | No | Per minute |
| A Mac mini of yours, as a self-hosted runner | Yes | One or more, plugged in | The Mac, once |
| A device farm | Some | Yes, rented | Per device-minute or a plan |

A hosted runner is the easiest start and needs no hardware. The Mac mini is the one that gets you a real phone in CI, and it has no per-run cost. Mobster’s [CI guide](https://docs.mobster.dev/ci) sets one up start to finish: a standard `ci` user with automatic login, `sudo pmset -a sleep 0 disksleep 0 autorestart 1` so the Mac never sleeps through a job, and the runner installed as a LaunchAgent of that user so it reaches the user’s simulators and iPhone.

## Step 3: add Mobster’s checks to the pull request

Mobster tests the screens a pull request changed, then keeps one comment up to date with the verdict. From your project’s folder:

```
mobster ci init
```

```
Wrote .github/workflows/mobster.yml.Next:  1. On the Mac that runs the checks: mobster ci doctor  2. Register it as a self-hosted runner with the label mobster: https://docs.mobster.dev/ci/mac-runner  3. Add ANTHROPIC_API_KEY as a repository secret, for checks with steps.  4. Commit the file and open a pull request.
```

It writes a workflow that runs on your `mobster` runner, so your other jobs stay on hosted runners:

```
name: mobsteron: pull_requestpermissions:  contents: read  pull-requests: writejobs:  checks:    runs-on: [self-hosted, macOS, mobster]    timeout-minutes: 60    steps:      - uses: actions/checkout@v4        with:          fetch-depth: 0      - run: /Applications/Mobster.app/Contents/MacOS/mobster-agent ci run
```

The file `mobster ci init` writes has more: the report upload, the comment step and a concurrency group. `mobster ci run` fetches the base branch and runs `mobster test pr --video`. It reads the diff, picks the saved checks that cover the changed screens, builds the app, runs the checks, and drafts a check for each changed screen that has none. It fails the job when a check fails or couldn’t run.

The comment names what failed, with a folded repro for each check. An example:

```
### Mobster checks: 11 passed, 1 failed| Check | Device | Expected | Found | Frame || --- | --- | --- | --- | --- || Failed: The paywall shows three plans | iPhone 17 Pro, iOS 26.4 | text “Choose your plan” | not on screen; closest: "Choose a plan" | paywall.jpg |
```

Each failed check in the comment carries the device, the deep link it opened, its steps, what it expected and found, a GIF, the video and the command that runs it again. Typed text, email addresses and phone numbers show as `•••`. The `junit.xml` works with any JUnit reader, and GitLab shows it in the merge request by itself.

Launch-only checks need no key. A check with steps runs on your `ANTHROPIC_API_KEY` until it passes once, then can replay with no model call. Checks that use project secrets run Smart fresh.

## Step 4: add a real iPhone and a nightly run

Plug a test phone into the runner Mac, set it up once in Mobster for Mac, and set Auto-Lock to Never. Then list it in the runner’s own environment file, not in the workflow, because a pull request can edit the workflow:

```
# ~/actions-runner/.envMOBSTER_RUNNER_DEVICES=Test iPhoneMOBSTER_RUNNER_TESTFLIGHT=com.example.yourapp
```

```
mobster ci run --device "Test iPhone" --testflight com.example.yourapp
```

A device or TestFlight app the runner doesn’t list stops the job before a check runs. With several phones on one runner, add `-- --spread` after the `mobster ci run` arguments to give each check to whichever phone is free.

Run every check, not only the changed screens, overnight:

```
mobster test nightly on --at 02:00 --device all
```

That writes a LaunchAgent. In the morning, `.mobster/nightly/<date>/morning.md` has the verdict, each failed or flaky check with the reason, and the command that runs it again. To split one suite across several Macs, use `--shard 1/3`, `--shard 2/3` and `--shard 3/3` with a matrix strategy.

## What this doesn’t do

- **No phone fleet.** You test the iPhones you own. If you must cover 40 physical models and iOS versions at once, rent a farm for that, and keep the Mac for every pull request.
- **No hosted dashboard.** Mobster hosts no runners or phones. Reports are files in your CI artifacts and your project folder.
- **A self-hosted runner runs the code in each pull request.** Don’t use one on a public repository that takes pull requests from forks, and use a test phone on a test account. [GitHub’s hardening guide](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#hardening-for-self-hosted-runners) explains why.
- **On a phone, a check drives only your own build,** never an App Store app, and never clears the app’s data.
- **The runner Mac needs Mobster for Mac, signed in, with a plan or the free trial.** A plan is $12 a month, $99 a year or $149 once, with every phone and every run included.

[Download Mobster for Mac](https://mobster.dev/download) to run `mobster ci init` on your own Mac mini: [$12 a month, $99 a year, or $149 once](https://mobster.dev/#pricing), with no per-run or per-device fee.

[Running checks in CI](https://docs.mobster.dev/ci) has every `mobster ci` flag and the GitLab setup, and [Checks](https://docs.mobster.dev/checks) explains the check file.

## Questions

- ### Do I need a device farm to run iOS UI tests in CI?
  Not to catch most regressions. Simulators on a Mac cover your screens across device sizes and iOS versions, and one real iPhone on the same Mac covers what simulators miss. A farm earns its cost when you must test many physical models at once.
- ### Can GitHub-hosted runners run iOS UI tests?
  Yes, on a macOS runner with Xcode and simulators. They have no real iPhone attached, so real-device checks need a Mac of your own with the phone plugged in.
- ### Is it safe to plug a phone into a self-hosted runner?
  Only with a test phone on a test account, and not on a public repository that takes pull requests from forks, because a pull request can run code on the runner. Mobster lists the phones a runner may test in the runner's own environment file, which a pull request can't edit.

## Sources

We read each of these on 17 October 2026. Reviewed by Mobster team on 18 October 2026. Corrections are welcome at the [GitHub repo](https://github.com/RadishSoftware/mobster/issues).

- [Apple: running tests and viewing results](https://developer.apple.com/documentation/xcode/running-tests-and-viewing-results)
- [GitHub: hardening self-hosted runners](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#hardening-for-self-hosted-runners)
- [Mobster docs: running checks in CI](https://docs.mobster.dev/ci)
- [Mobster docs: a Mac as your CI runner](https://docs.mobster.dev/ci/mac-runner)
- [Mobster docs: run every check with mobster test](https://docs.mobster.dev/testing)

## Try it on your own iPhone

Mobster for Mac is $12 a month, $99 a year, or $149 once, with 7 days free. It sets up your iPhone and adds itself to Claude Code, Cursor or Codex.

[Download Mobster for Mac](https://mobster.dev/download) [Pricing](https://mobster.dev/#pricing)

## More from the blog

### [Maestro vs XCUITest vs Mobster: when to use which](https://mobster.dev/blog/maestro-vs-xcuitest-vs-mobster)

Comparisons 7 Oct 2026

### [How to test an iOS app with an AI agent](https://mobster.dev/blog/ai-e2e-tests-ios-assertions)

iOS testing 7 Oct 2026

### [Test your iOS app on a real iPhone with an AI agent](https://mobster.dev/blog/test-ios-app-real-iphone-ai-agent)

iOS testing 17 Oct 2026
