UI tests fail noisily when they ask for a screen element before the app has finished rendering, loading data, or removing a blocking overlay. XCUITest element waits let the test wait for the named UI state instead of sleeping for a fixed number of seconds.

Use the built-in appearance wait for elements that need to appear, and use the non-existence wait for elements that need to disappear. For tappable controls, a predicate wait against isHittable catches the extra state where the element exists but is still offscreen, covered, or not ready for a tap.

The app should expose stable accessibility identifiers such as sign-in-button, welcome-title, and login-progress before the UI test relies on waits. Keep wait timeouts close to the expected UI transition, and let the test fail with the element name when the expected state never arrives.

Steps to wait for UI elements in XCUITest:

  1. Add an element wait helper to the UI test target.
    WaitForElement.swift
    import Foundation
    import XCTest
     
    extension XCUIElement {
        @discardableResult
        func requireExistence(
            _ name: String,
            timeout: TimeInterval = 5,
            file: StaticString = #filePath,
            line: UInt = #line
        ) -> XCUIElement {
            XCTAssertTrue(
                waitForExistence(timeout: timeout),
                "Timed out after \(timeout)s waiting for \(name) to exist.",
                file: file,
                line: line
            )
            return self
        }
     
        @discardableResult
        func requireHittability(
            _ name: String,
            timeout: TimeInterval = 5,
            file: StaticString = #filePath,
            line: UInt = #line
        ) -> XCUIElement {
            let predicate = NSPredicate(format: "hittable == true")
            let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self)
            let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
     
            XCTAssertEqual(
                result,
                .completed,
                "Timed out after \(timeout)s waiting for \(name) to become hittable.",
                file: file,
                line: line
            )
            return self
        }
     
        func requireNonExistence(
            _ name: String,
            timeout: TimeInterval = 5,
            file: StaticString = #filePath,
            line: UInt = #line
        ) {
            XCTAssertTrue(
                waitForNonExistence(timeout: timeout),
                "Timed out after \(timeout)s waiting for \(name) to disappear.",
                file: file,
                line: line
            )
        }
    }

    waitForExistence(timeout:) and waitForNonExistence(timeout:) wait for existence state. The XCTNSPredicateExpectation branch is for the tappable state exposed through isHittable. Projects pinned to an older Xcode can use a predicate wait for exists == false until the toolchain is upgraded.

  2. Stop the test after the first required UI state fails.
    LoginUITests.swift
    import XCTest
     
    final class LoginUITests: XCTestCase {
        override func setUpWithError() throws {
            continueAfterFailure = false
        }
    }

    Required waits return the element so the test code stays readable, but continueAfterFailure = false prevents later taps from running after a required wait has already failed.

  3. Wait for the first screen element before interacting with the app.
    LoginUITests.swift
    func testLoginWaitsForWelcome() throws {
        let app = XCUIApplication()
        app.launch()
     
        app.buttons["sign-in-button"]
            .requireHittability("Sign In button", timeout: 5)
            .tap()
    }

    Use a hittability wait before tap() when a button may exist before it can receive touches.

  4. Wait for the loading state to disappear.
    LoginUITests.swift
    let progress = app.activityIndicators["login-progress"]
    progress.requireNonExistence("login progress indicator", timeout: 10)

    Waiting for disappearance is clearer than sleeping while the network request or animation finishes.

  5. Wait for the final screen state before asserting it.
    LoginUITests.swift
    let welcomeTitle = app.staticTexts["welcome-title"]
    welcomeTitle.requireExistence("Welcome title", timeout: 5)
    XCTAssertEqual(welcomeTitle.label, "Welcome")

    Keep the wait and the assertion separate. The wait proves the element is present; the assertion proves the visible state is the expected one.

  6. Run the focused UI test from xcodebuild.
    $ xcodebuild test \
      -workspace MyApp.xcworkspace \
      -scheme MyApp \
      -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
      -only-testing:MyAppUITests/LoginUITests/testLoginWaitsForWelcome
    Testing started
    Test Suite 'LoginUITests' passed
    Executed 1 test, with 0 failures (0 unexpected)
    ** TEST SUCCEEDED **

    Run the same test with a temporarily wrong identifier only when you need to verify the failure text. The helper should report the named state, such as Timed out after 5.0s waiting for Welcome title to exist.