Recording UI Automation (Xcode 26+)
Guide to Xcode 26's Recording UI Automation feature for creating UI tests through user interaction recording.
The Three-Phase Workflow
From WWDC 2025-344:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ UI Automation Workflow โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ 1. RECORD โโโโโโโบ Interact with app in Simulator โ
โ Xcode captures as Swift test code โ
โ โ
โ 2. REPLAY โโโโโโโบ Run across devices, languages, configs โ
โ Using test plans for multi-config โ
โ โ
โ 3. REVIEW โโโโโโโบ Watch video recordings in test report โ
โ Analyze failures with screenshots โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Phase 1: Recording
Starting a Recording
- Open your UI test file in Xcode
- Place cursor inside a test method
- Debug โ Record UI Automation (or use the record button)
- App launches in Simulator
- Perform interactions - Xcode generates code
- Stop recording when done
What Gets Recorded
- Taps on buttons, cells, controls
- Text input into text fields
- Swipes and scrolling
- Gestures (pinch, rotate)
- Hardware button presses (Home, volume)
Generated Code Example
func testLoginFlow() {
let app = XCUIApplication()
app.launch()
app.textFields["Email"].tap()
app.textFields["Email"].typeText("[email protected]")
app.secureTextFields["Password"].tap()
app.secureTextFields["Password"].typeText("password123")
app.buttons["Login"].tap()
}
Enhancing Recorded Code
Critical: Recorded code is often fragile. Always enhance it for stability.
1. Add Accessibility Identifiers
Recorded code uses labels which break with localization:
app.buttons["Login"].tap()
app.buttons["loginButton"].tap()
Add identifiers in your app code:
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
loginButton.accessibilityIdentifier = "loginButton"
2. Add waitForExistence
Recorded code assumes elements exist immediately:
app.buttons["Login"].tap()
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(loginButton.waitForExistence(timeout: 5))
loginButton.tap()
3. Add Assertions
Recorded code just performs actions without verification:
app.buttons["Login"].tap()
app.buttons["loginButton"].tap()
let welcomeLabel = app.staticTexts["welcomeLabel"]
XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 10),
"Welcome screen should appear after login")
4. Use Shorter Queries
Recorded code may have overly specific queries:
app.tables.cells.element(boundBy: 0).buttons["Action"].tap()
app.buttons["actionButton"].tap()
Query Selection Guidelines
From WWDC 2025-344:
| Scenario |
Problem |
Solution |
| Localized strings |
"Login" changes by language |
Use accessibilityIdentifier |
| Deeply nested views |
Long query chains break easily |
Use shortest possible query |
| Dynamic content |
Cell content changes |
Use identifier or generic query |
| Multiple matches |
Query returns many elements |
Add unique identifier |
Best Practices
- Prefer identifiers over labels
- Use the shortest query that works
- Avoid index-based queries (
element(boundBy: 0))
- Add identifiers to dynamic content
Phase 2: Replay with Test Plans
Test plans allow running the same tests across multiple configurations.
Creating a Test Plan
- File โ New โ File โ Test Plan
- Add test targets
- Configure configurations
Test Plan Structure
{
"configurations": [
{
"name": "iPhone - English",
"options": {
"targetForVariableExpansion": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyApp"
},
"language": "en",
"region": "US"
}
},
{
"name": "iPhone - Spanish",
"options": {
"language": "es",
"region": "ES"
}
},
{
"name": "iPhone - Dark Mode",
"options": {
"userInterfaceStyle": "dark"
}
},
{
"name": "iPad - Landscape",
"options": {
"defaultTestExecutionTimeAllowance": 120,
"testTimeoutsEnabled": true
}
}
],
"defaultOptions": {
"targetForVariableExpansion": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyApp"
}
},
"testTargets": [
{
"target": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyAppUITests",
"name": "MyAppUITests"
}
}
],
"version": 1
}
Configuration Options
| Option |
Purpose |
language |
Test localization |
region |
Test regional formatting |
userInterfaceStyle |
Test dark/light mode |
targetForVariableExpansion |
App target for configuration |
testTimeoutsEnabled |
Enable timeout enforcement |
defaultTestExecutionTimeAllowance |
Timeout in seconds |
Running with Test Plan
xcodebuild test \
-scheme "MyApp" \
-testPlan "MyTestPla