XCTest drives the app through the Accessibility APIs, so UI tests work against SwiftUI and UIKit alike, and against a coupled codebase no unit test can reach. A UI test scripts one user flow and asserts what the screen shows.
Test Case Shape
Create the target with File > New > Target > UI Testing Bundle. Every method starting with
test runs as a test; setUp runs before each one and relaunches the app so runs stay
consistent.
final class UITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["testing"]
app.launch()
}
func testWelcomeMessage() {
XCTAssertTrue(app.staticTexts["Hello World!"].exists)
}
}
Finding and Driving Views
Query by element type and subscript; the string matches the accessibility label or identifier. Buttons, labels, and switches use their titles automatically; lists and collections need an explicit identifier.
let email = app.textFields["email"]
email.tap()
email.typeText("[email protected]")
app.switches["rememberMe"].tap()
app.buttons["login"].tap()
// SwiftUI side:
List { Text("news item") }
.accessibilityIdentifier("newsList")
SecureField("password", text: $viewModel.password)
.accessibilityLabel("password")
Assert on async outcomes with waitForExistence(timeout:), which returns true as soon as
the element appears and false when the timeout passes. Plain .exists checks only the
current instant.
XCTAssertTrue(app.staticTexts["Hello World!"].waitForExistence(timeout: 5))
Speed and Isolation
UI tests boot the whole app, so keep them for the crucial flows and let unit tests carry the volume. Use the launch argument to detect a test run, then reset state (user defaults, keychain, local database) and disable animations.
func applicationDidFinishLaunching(_ application: UIApplication) {
if CommandLine.arguments.contains("testing") {
UIView.setAnimationsEnabled(false)
}
}