Mobile Testing

XCUIElement: Finding and Interacting with UI Elements

Master XCUIElement in XCUITest with practical Swift examples for finding, interacting with, synchronizing, and validating iOS UI elements in reliable automation tests.

12 min read
XCUIElement: Finding and Interacting with UI Elements
Advertisement
What You Will Learn
What is XCUIElement?
Key Points
XCUIElement vs XCUIElementQuery
Finding UI Elements
⚑ Quick Answer
XCUIElement is the core XCUITest object QA engineers and SDETs use to find, inspect, and interact with UI elements in iOS applications. You leverage XCUIElementQuery to reliably locate elements, ideally using stable accessibility identifiers, before performing actions like tapping or typing text. Mastering XCUIElement ensures robust UI automation, effective synchronization, and maintainable test frameworks.

XCUIElement is the core XCUITest object for locating and interacting with UI elements in an iOS application. For SDETs, understanding XCUIElement means moving beyond simple tap() calls toward reliable element queries, accessibility identifiers, synchronization, state validation, and maintainable page-object architecture.

What is XCUIElement?

XCUIElement represents a UI element in the application hierarchy exposed through XCUITest. It provides APIs for querying, inspecting, and interacting with elements such as buttons, text fields, labels, images, switches, cells, and other controls.

A basic interaction looks like this:

JavaScript
let app = XCUIApplication()

app.launch()

let loginButton = app.buttons["loginButton"]

loginButton.tap()

The architecture is:

Code
XCTestCase
    ↓
XCUIApplication
    ↓
XCUIElementQuery
    ↓
XCUIElement
    ↓
Interaction / Assertion

XCUIApplication controls the application.

XCUIElement represents the specific UI object that the test needs to inspect or manipulate.

Key Points

  • XCUIElement represents a UI element.
  • Elements are normally obtained through XCUIElementQuery.
  • Accessibility identifiers provide stable selectors.
  • exists checks whether an element currently exists.
  • isHittable checks whether an element can currently receive interaction.
  • tap() performs a tap interaction.
  • typeText() enters text into supported elements.
  • clearText() removes existing text from supported text fields.
  • waitForExistence(timeout:) provides condition-based synchronization.
  • XCUIElement supports assertions against UI state.
  • Reliable selectors are more important than simply finding an element.

XCUIElement vs XCUIElementQuery

These concepts are closely related but have different responsibilities.

XCUIElementQuery

A query identifies elements:

JavaScript
let loginButton = app.buttons["loginButton"]

XCUIElement

The resulting object represents the element that the test can inspect or interact with:

Code
loginButton.tap()

Conceptually:

Code
XCUIElementQuery
       ↓
Find
       ↓
XCUIElement
       ↓
Inspect / Interact

This distinction becomes important when designing scalable XCUITest frameworks.

Finding UI Elements

XCUITest provides several query strategies.

Buttons

JavaScript
let loginButton = app.buttons["loginButton"]

Text Fields

JavaScript
let emailField = app.textFields["emailField"]

Secure Text Fields

JavaScript
let passwordField = app.secureTextFields["passwordField"]

Static Text

JavaScript
let welcomeMessage = app.staticTexts["Welcome"]

Images

JavaScript
let logo = app.images["appLogo"]

Switches

JavaScript
let notificationsSwitch = app.switches["notificationsSwitch"]

Cells

JavaScript
let firstCell = app.cells.element(boundBy: 0)

The selector should describe the UI element’s automation identity rather than relying on its visual position.

Accessibility Identifiers

For production-grade automation, accessibility identifiers are usually the preferred way to create stable selectors.

In the application:

Code
loginButton.accessibilityIdentifier = "loginButton"

In XCUITest:

JavaScript
let loginButton = app.buttons["loginButton"]

loginButton.tap()

This is significantly more maintainable than depending on visible text:

Code
app.buttons["Log In"].tap()

Why?

The visible text might change because of:

  • Localization
  • Product wording changes
  • A/B testing
  • Branding changes
  • UX redesign

The automation identifier can remain stable.

Recommended Selector Hierarchy

For maintainable XCUITest automation, prefer selectors roughly in this order:

Code
Accessibility Identifier
        ↓
Accessibility Label / Exact Text
        ↓
Predicate Query
        ↓
Hierarchy-Based Query
        ↓
Index-Based Query

The further down the list you go, the more fragile the selector can become.

For example:

Code
app.buttons["loginButton"]

is generally preferable to:

Code
app.buttons.element(boundBy: 3)

because the third button can change when the UI changes.

Interaction object between an XCUITest automation layer and a realistic iOS application
Interaction object between an XCUITest automation layer and a realistic iOS application

Interacting With XCUIElement

Once an element has been located, XCUITest provides interaction APIs.

Tap

JavaScript
let loginButton = app.buttons["loginButton"]

loginButton.tap()

Double Tap

Code
loginButton.doubleTap()

Long Press

Code
loginButton.press(forDuration: 2)

Swipe

Code
app.swipeUp()

For element-specific gestures:

Advertisement
Code
loginButton.swipeLeft()

The interaction should normally occur only after the test has established that the element is available.

Waiting for an Element

One of the most important synchronization APIs is:

Code
waitForExistence(timeout:)

Example:

JavaScript
let loginButton = app.buttons["loginButton"]

XCTAssertTrue(
    loginButton.waitForExistence(timeout: 10)
)

loginButton.tap()

This is better than:

Code
sleep(5)

loginButton.tap()

The difference is fundamental.

Code
sleep()
    ↓
Wait fixed duration

waitForExistence()
    ↓
Wait until condition becomes true

Condition-based synchronization generally produces more resilient automation.

Checking Element Existence

You can inspect:

Code
if loginButton.exists {
    loginButton.tap()
}

For assertions:

Code
XCTAssertTrue(
    loginButton.exists
)

However, exists is an immediate state check.

For asynchronous UI loading, prefer:

Code
XCTAssertTrue(
    loginButton.waitForExistence(timeout: 10)
)

Checking Whether an Element Is Hittable

An element can exist without being interactable.

Use:

Code
XCTAssertTrue(
    loginButton.isHittable
)

For example, an element could be:

  • Behind another view
  • Outside the visible viewport
  • Disabled
  • Not currently actionable
  • Part of a transition

Therefore:

Code
exists

and:

Code
isHittable

answer different questions.

Code
exists
  ↓
Does the element exist?

isHittable
  ↓
Can the element currently receive interaction?

Reading Element Properties

XCUIElement exposes properties that can be useful for assertions and diagnostics.

Example:

JavaScript
let emailField = app.textFields["emailField"]

XCTAssertTrue(emailField.exists)
XCTAssertTrue(emailField.isEnabled)
XCTAssertTrue(emailField.isHittable)

A test can therefore validate both presence and usability.

Typing Into Text Fields

A common XCUITest workflow is:

JavaScript
let emailField = app.textFields["emailField"]

emailField.tap()
emailField.typeText("qa@example.com")

For password fields:

JavaScript
let passwordField = app.secureTextFields["passwordField"]

passwordField.tap()
passwordField.typeText("Secret123!")

The test should identify the field using a stable selector.

Clearing Existing Text

When supported by the SDK and element type, text can be cleared before entering a new value.

For example:

Code
emailField.tap()
emailField.clearText()
emailField.typeText("new@example.com")

This is preferable to blindly appending text to an existing value.

Keyboard Interaction

After entering text, the keyboard may affect subsequent interactions.

For example:

Advertisement
Code
emailField.tap()
emailField.typeText("qa@example.com")

app.keyboards.buttons["Return"].tap()

The exact keyboard hierarchy can vary depending on the application and keyboard configuration, so selectors should be verified against the accessibility hierarchy.

Element Queries

XCUITest allows elements to be queried by type.

Examples:

Code
app.buttons
app.textFields
app.staticTexts
app.images
app.cells
app.switches
app.sliders

You can then select an element:

JavaScript
let firstButton = app.buttons.element(boundBy: 0)

Or identify it directly:

JavaScript
let loginButton = app.buttons["loginButton"]

Direct identification is usually preferable when a stable identifier is available.

Predicate-Based Queries

For more advanced selection logic, use predicates.

Example:

JavaScript
let predicate = NSPredicate(
    format: "label BEGINSWITH 'Login'"
)

let loginElement = app.buttons.element(
    matching: predicate
)

Predicate queries are useful when simple identifier-based selection is insufficient.

However, avoid unnecessarily complex predicates.

A selector should be:

  • Stable
  • Readable
  • Specific
  • Easy to diagnose

Matching Multiple Elements

Sometimes an application contains multiple elements matching the same selector.

You can inspect the query:

JavaScript
let buttons = app.buttons["actionButton"]

print(buttons.count)

Then access a specific element:

JavaScript
let firstButton = buttons.element(boundBy: 0)

However, relying heavily on indexes makes tests sensitive to UI ordering.

A better application design provides unique accessibility identifiers where possible.

Collection and Table Testing

For table-based interfaces:

JavaScript
let cells = app.cells

XCTAssertGreaterThan(
    cells.count,
    0
)

You can locate a cell:

JavaScript
let profileCell = app.cells["profileCell"]

profileCell.tap()

For dynamic lists, stable identifiers are especially important.

Avoid:

Code
app.cells.element(boundBy: 7).tap()

unless the test specifically validates position.

Handling Dynamic UI

Modern iOS applications frequently load content asynchronously.

A weak approach:

Code
sleep(3)

app.buttons["Continue"].tap()

A stronger approach:

JavaScript
let continueButton = app.buttons["continueButton"]

XCTAssertTrue(
    continueButton.waitForExistence(timeout: 15)
)

continueButton.tap()

For more advanced workflows, combine existence and interaction state:

Code
XCTAssertTrue(
    continueButton.waitForExistence(timeout: 15)
)

XCTAssertTrue(
    continueButton.isHittable
)

continueButton.tap()

This makes the synchronization intent explicit.

Assertions With XCUIElement

UI tests should validate outcomes, not simply execute interactions.

Weak:

Advertisement
Code
app.buttons["loginButton"].tap()

Stronger:

Code
app.buttons["loginButton"].tap()

XCTAssertTrue(
    app.staticTexts["Dashboard"].waitForExistence(
        timeout: 10
    )
)

The second test validates the business outcome.

A production test should generally follow:

Code
Arrange
   ↓
Find Element
   ↓
Interact
   ↓
Wait for Result
   ↓
Assert Outcome

XCUIElement in Page Object Model

For larger frameworks, page objects can encapsulate element definitions.

Example:

Code
final class LoginPage {

    private let app: XCUIApplication

    init(app: XCUIApplication) {
        self.app = app
    }

    var emailField: XCUIElement {
        app.textFields["emailField"]
    }

    var passwordField: XCUIElement {
        app.secureTextFields["passwordField"]
    }

    var loginButton: XCUIElement {
        app.buttons["loginButton"]
    }

    func login(
        email: String,
        password: String
    ) {
        emailField.tap()
        emailField.typeText(email)

        passwordField.tap()
        passwordField.typeText(password)

        loginButton.tap()
    }
}

The test becomes:

JavaScript
func testSuccessfulLogin() {

    let loginPage = LoginPage(app: app)

    loginPage.login(
        email: "qa@example.com",
        password: "Secret123!"
    )

    XCTAssertTrue(
        app.staticTexts["Dashboard"]
            .waitForExistence(timeout: 10)
    )
}

This separates:

Code
Test Intent
    ↓
Page Object
    ↓
XCUIElement
    ↓
Application UI

6 Core Pillars of XCUIElement

1. Element Discovery

Find UI elements using reliable queries.

2. Stable Identification

Prefer accessibility identifiers over positional selectors.

3. Interaction

Use actions such as tap, press, type, swipe, and other supported gestures.

4. Synchronization

Wait for application conditions instead of relying on arbitrary delays.

5. State Validation

Validate existence, hittability, enabled state, and expected outcomes.

6. Framework Architecture

Encapsulate elements and interactions in maintainable page or screen objects.

Framework Architecture: XCTestCase
Framework Architecture: XCTestCase

Key Architectural Takeaways for SDETs

Use Accessibility IDs as an Automation Contract

The development and QA teams should treat accessibility identifiers as part of the application’s automation contract.

For example:

Code
button.accessibilityIdentifier = "checkoutButton"

Then:

Code
app.buttons["checkoutButton"].tap()

This creates a stable interface between the application and automation framework.

Separate Discovery From Interaction

Avoid deeply nested one-line expressions:

Code
app.buttons["loginButton"].tap()

when debugging complex flows.

Prefer:

JavaScript
let loginButton = app.buttons["loginButton"]

XCTAssertTrue(
    loginButton.waitForExistence(timeout: 10)
)

loginButton.tap()

The latter provides better diagnostics.

Build Reusable Screen Objects

Instead of duplicating:

Code
app.textFields["emailField"]

throughout hundreds of tests, expose it through a page object.

Synchronize With UI Conditions

Use:

Code
waitForExistence(timeout:)

instead of fixed delays whenever possible.

Validate Outcomes

The purpose of UI automation is not merely to click controls.

It should verify meaningful application behavior.

Advertisement
Architecture visualization showing a production-grade XCUIElement automation framework
Architecture visualization showing a production-grade XCUIElement automation framework

Common XCUIElement Mistakes

Mistake 1: Using Indexes as Primary Selectors

Avoid:

Code
app.buttons.element(boundBy: 2).tap()

Prefer:

Code
app.buttons["checkoutButton"].tap()

Mistake 2: Using Fixed Sleeps

Avoid:

Code
sleep(5)

Prefer:

Code
XCTAssertTrue(
    element.waitForExistence(timeout: 10)
)

Mistake 3: Assuming Existence Means Interactivity

An element can exist but not be hittable.

Validate appropriately:

Code
XCTAssertTrue(element.exists)
XCTAssertTrue(element.isHittable)

Mistake 4: Coupling Tests to Visible Text

Instead of:

Code
app.buttons["Submit Order"].tap()

consider:

Code
app.buttons["submitOrderButton"].tap()

when the application provides a stable accessibility identifier.

Mistake 5: Putting All Selectors Directly in Tests

Large suites become difficult to maintain when every test contains raw element queries.

Use screen/page objects.

Production-Ready XCUIElement Pattern

A practical screen object can combine selectors, synchronization, interactions, and assertions:

Code
final class LoginPage {

    private let app: XCUIApplication

    init(app: XCUIApplication) {
        self.app = app
    }

    private var emailField: XCUIElement {
        app.textFields["emailField"]
    }

    private var passwordField: XCUIElement {
        app.secureTextFields["passwordField"]
    }

    private var loginButton: XCUIElement {
        app.buttons["loginButton"]
    }

    func verifyLoaded() {
        XCTAssertTrue(
            emailField.waitForExistence(timeout: 10)
        )

        XCTAssertTrue(
            passwordField.exists
        )

        XCTAssertTrue(
            loginButton.exists
        )
    }

    func login(
        email: String,
        password: String
    ) {
        emailField.tap()
        emailField.typeText(email)

        passwordField.tap()
        passwordField.typeText(password)

        XCTAssertTrue(
            loginButton.isHittable
        )

        loginButton.tap()
    }
}

Test:

JavaScript
func testSuccessfulLogin() {

    let loginPage = LoginPage(app: app)

    loginPage.verifyLoaded()

    loginPage.login(
        email: "qa@example.com",
        password: "Secret123!"
    )

    XCTAssertTrue(
        app.staticTexts["Dashboard"]
            .waitForExistence(timeout: 10)
    )
}

This approach creates a clear separation between test intent and UI implementation.

AI Overview & Answer Engine Optimization

XCUIElement is the XCUITest representation of a UI element that enables automated tests to query, inspect, interact with, and validate elements in an iOS application.

Key Points

  • XCUIElement represents an individual UI element.
  • XCUIElementQuery is used to find elements.
  • Accessibility identifiers provide stable selectors.
  • exists checks element presence.
  • isHittable checks whether an element can receive interaction.
  • waitForExistence(timeout:) provides condition-based synchronization.
  • tap(), typeText(), and gesture APIs enable interaction.
  • Page Object Model improves maintainability.

How Do You Find an Element in XCUITest?

Use an element query:

JavaScript
let loginButton = app.buttons["loginButton"]

How Do You Tap an XCUIElement?

JavaScript
let loginButton = app.buttons["loginButton"]

XCTAssertTrue(
    loginButton.waitForExistence(timeout: 10)
)

loginButton.tap()

How Do You Check Whether an XCUIElement Exists?

Code
XCTAssertTrue(
    app.buttons["loginButton"].exists
)

How Do You Wait for an XCUIElement?

Code
XCTAssertTrue(
    app.buttons["loginButton"]
        .waitForExistence(timeout: 10)
)

What is the Best Selector for XCUIElement?

For production XCUITest automation, a stable accessibility identifier is generally preferable to positional selectors such as element(boundBy:).

AI Overview Summary

XCUIElement is the core XCUITest object for interacting with iOS UI elements. SDETs use XCUIElement with XCUIElementQuery to locate controls, validate UI state, perform actions, synchronize with dynamic interfaces, and build maintainable Page Object Model automation.

People Asked Questions

What is XCUIElement?

XCUIElement represents an individual UI element exposed to XCUITest for inspection and interaction.

How do I find an XCUIElement?

Use an element query such as:

Code
app.buttons["loginButton"]

What is the difference between XCUIElement and XCUIApplication?

XCUIApplication represents and controls the application, while XCUIElement represents a specific UI element inside that application.

How do I tap an XCUIElement?

Code
app.buttons["loginButton"].tap()

How do I check if an XCUIElement exists?

Code
XCTAssertTrue(
    app.buttons["loginButton"].exists
)

What is isHittable?

isHittable indicates whether XCUITest considers the element currently capable of receiving user interaction.

Should I use element(boundBy:)?

Use it when position is genuinely what the test needs to validate. Avoid it as the primary selector strategy when stable identifiers are available.

Why use accessibility identifiers?

They provide a stable automation contract that is less sensitive to visible text, localization, and UI wording changes.

Should I use sleep() with XCUIElement?

Avoid fixed sleeps when possible. Prefer condition-based synchronization such as waitForExistence(timeout:).

Can XCUIElement be used in Page Object Model?

Yes. Screen/page objects can expose XCUIElement properties and encapsulate interactions, making large XCUITest suites easier to maintain.

Internal Blog Links

Internal Series Links

External Links


Continue Learning

Explore more expert articles on Mobile Testing, Backend & API, AI & Agentic, AI Tools, n8n, LangChain, CrewAI, MCP Servers, AI Agents, LlamaIndex, Docker, FastAPI, Playwright, Cypress, Test Automation, DevOps, and Software Engineering at www.skakarh.com.

QAPulse by SK delivers expert release analysis, AI engineering insights, enterprise automation strategies, migration guidance, DevOps best practices, and practical testing knowledge to help software professionals build scalable, intelligent, and production-ready software systems.

Frequently Asked Questions

What is XCUIElement in XCUITest?
XCUIElement is the core XCUITest object for locating and interacting with UI elements in an iOS application. It represents a UI element in the application hierarchy and provides APIs for querying, inspecting, and interacting with elements like buttons, text fields, and labels.
How does XCUIElement relate to XCUIElementQuery?
XCUIElementQuery identifies elements, while XCUIElement represents the resulting object that the test can inspect or interact with. Conceptually, an XCUIElementQuery finds an XCUIElement, which is then used for inspection or interaction.
What are some key capabilities of XCUIElement for QA engineers?
XCUIElement allows QA engineers to perform interactions like tap() and typeText(), check for element existence with exists, and verify interactability with isHittable. It also supports assertions against UI state and condition-based synchronization using waitForExistence(timeout:).
Advertisement
Found this helpful? Clap to let Shahnawaz know β€” you can clap up to 50 times.