
Direct recommendation. For an automated iOS accessibility audit that runs inside an XCUITest target, request an M-series Mac from Figment (from $95/month) and call XCUIApplication.performAccessibilityAudit(for:_:) in a UI-test target. The API lives in the XCUIAutomation framework and is available from Xcode 16.3, iOS 17, macOS 14, tvOS 17 and watchOS 10 (Apple: Performing accessibility audits for your app, WWDC23 session 10035). Start a request at /mac-hosting/start.
The $49 i3 Mac and the $29 virtual Intel macOS plan can run a compatible Intel Xcode toolchain and the iOS Simulator. The $29 VM has no GPU acceleration and no audio, and it is not an Apple silicon Mac; ARM-only binaries and Xcode 27 require an M-series host. We have not measured simulator or audit speed across tiers, so match the chip to the target architecture and your own test budget. Access on the $29 VM is through the browser console; access, software image and exact hardware on physical Intel and M-series plans are confirmed in the quote.
performAccessibilityAudit(for:_:) walks every accessible element currently visible and returns one XCUIAccessibilityAuditIssue per problem Apple's auditor finds. It is the same set of checks Accessibility Inspector runs when you press its Audit button, exposed from code so CI can fail a pull request on a regression. You can scope the sweep to one or more XCUIAccessibilityAuditType values or pass .all.
| Audit type | What it flags |
|---|---|
.contrast | Text with contrast below readable thresholds |
.dynamicType | Text that does not scale with the user's preferred size |
.elementDetection | Views the system cannot resolve as a single element |
.hitRegion | Interactive elements the auditor judges too small to activate reliably |
.sufficientElementDescription | Controls with missing or empty accessibility labels |
.textClipped | Labels that truncate at larger accessibility text sizes |
.trait | Missing or inconsistent accessibility traits |
Apple owns the exact thresholds (minimum hit-region size, contrast ratio) and may move them between OS versions. Treat the API's verdict as authoritative in CI; do not hard-code pixel or ratio numbers into your tests to try to pre-compute what it will say.
Drop this into your UI-test target. Replace the screen-driving lines, the button label and the suppressed identifier with your own project's values — the audit is only as useful as the screens you walk it through.
import XCTest // XCUIAutomation ships with XCTest UI-test targets.
final class AccessibilityAuditTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = true // Collect every issue in one run.
app.launchArguments = ["--ui-testing"]
app.launch()
}
// A plain sweep: fails the test on any issue returned by the audit.
func testLaunchScreenAudit() throws {
try app.performAccessibilityAudit() // defaults to .all
}
// A focused sweep with a narrowly typed suppression.
func testOnboardingAudit() throws {
app.buttons["Get started"].tap()
try app.performAccessibilityAudit(for: .all) { issue in
// Suppress one known finding: the decorative logo's contrast
// rating, reviewed separately. Any other audit type on the
// same element still fails the test.
guard issue.auditType == .contrast,
let element = issue.element,
element.identifier == "decorative_app_logo"
else {
return false // report as a test failure
}
return true // ignore just this one finding
}
}
}
Three details to keep honest. First, the handler closure is the second positional parameter — there is no handler: label in Apple's signature. Second, issue.element is XCUIElement?, so unwrap before using it; the auditor can report an issue without a resolved element. Third, narrow suppressions by both auditType and element identifier; returning true for every finding on one element would silence every audit category against that element forever.
If you want a tight pre-submission tripwire rather than a full .all sweep, pass a focused option set:
try app.performAccessibilityAudit(for: [
.contrast,
.sufficientElementDescription,
.hitRegion
])
Access to the $29 virtual Intel macOS environment is through the browser console; outbound CI agents (GitHub Actions self-hosted runners, Buildkite, Bitrise) connect from the Mac to your pipeline. Confirm the macOS version, installed Xcode and disk you need when you request a physical Intel or M-series plan, so the audit target matches the OS your users run.
Once a runner is attached, the audit job is a plain xcodebuild invocation against your own workspace, scheme and simulator. The test target is the one you added above; everything else is a parameter you set from your project.
#!/usr/bin/env bash
set -euo pipefail
: "${WORKSPACE:?set to your .xcworkspace or .xcodeproj}"
: "${UI_TEST_SCHEME:?set to the scheme that contains AccessibilityAuditTests}"
: "${SIMULATOR:?example: platform=iOS Simulator,name=iPhone 16,OS=latest}"
xcodebuild test \
-workspace "$WORKSPACE" \
-scheme "$UI_TEST_SCHEME" \
-destination "$SIMULATOR" \
-only-testing:"$UI_TEST_SCHEME/AccessibilityAuditTests" \
-resultBundlePath "./audit.xcresult"
Upload audit.xcresult as a CI artifact and inspect it with Xcode or xcrun xcresulttool when a failure needs triage. The result bundle carries screenshots of the elements the auditor flagged, which is faster than rerunning the suite to find out which button lost contrast. Do not grep stdout for "failed" and call it green: let set -e propagate the non-zero exit from xcodebuild.
For manual follow-up on a flagged screen, open the browser console session, launch Xcode → Open Developer Tool → Accessibility Inspector, point it at the running simulator and click Audit to walk the element tree by hand.
Pick the audit host you plan to keep longest; the comparison is a year, not a month. Figment's $95 is an M-series entry price — the exact chip, RAM and disk are set in the quote — so treat the row below as an entry-price comparison, not a spec-for-spec match with CloudMac's M4/16GB/256GB listing.
| Option | Headline | Effective monthly | 12-month cost |
|---|---|---|---|
| Figment M-series Mac (entry) | from $95/month | $95.00 | $1,140.00 |
| CloudMac M4 / 16 GB / 256 GB, monthly | $99.99/month | $99.99 | $1,199.88 |
| CloudMac M4 / 16 GB / 256 GB, annual | $999/year prepaid | $83.25 | $999.00 |
Arithmetic check: 12 × $95 = $1,140.00; 12 × $99.99 = $1,199.88; $999 / 12 = $83.25; $1,140 − $999 = $141. The CloudMac annual prepay is $141 lower over twelve months on that specific M4/16GB/256GB machine, while Figment's entry price undercuts the monthly rate by $4.99. CloudMac's 256GB row currently reads "Reserve now," while its 512GB and 1TB rows read "Fully reserved — Next allocation not scheduled," so confirm availability before you prepay. If you want CI setup, backups, monitoring and 30 minutes of monthly support bundled on one M-series Mac, Managed Build Mac is $249/month.
Each audit type usually has a one-line fix. The UIKit column stays conservative on hit-region: the auditor measures the frame it can see, so grow the frame. accessibilityActivationPoint moves where VoiceOver taps the element — it does not enlarge the hit region. .contentShape(Rectangle()) changes the shape that receives gestures within the view's bounds; it does not expand the layout, so use it together with a larger frame if the view is too small.
| Audit failure | SwiftUI fix | UIKit fix |
|---|---|---|
.sufficientElementDescription | .accessibilityLabel("Buy ticket") | view.accessibilityLabel = "Buy ticket" |
.hitRegion | enlarge the tappable view, e.g. .frame(minWidth: 44, minHeight: 44) | enlarge the view's frame or add a transparent hit view sized up to the target |
.contrast | adjust foreground/background colour tokens | adjust UIColor assets and dark-mode variants |
.trait | .accessibilityAddTraits(.isButton) | view.accessibilityTraits.insert(.button) |
.dynamicType | .font(.body) and other scalable text styles | UIFontMetrics.default.scaledFont(for:) |
.textClipped | allow multiline or set .minimumScaleFactor | enable multiline / shrink-to-fit on the label |
Run the audit on every pull request and review each suppression in code review. That shifts the noisy accessibility work off reviewers and before the submission build, even if it does not guarantee that every regression is caught — the API only inspects visible elements on the screens your tests walk through. Start your rental at /mac-hosting/start.
Related guides: