Platform-Specific Locators — UIAutomator2 and XCUITest

9 min read

When Accessibility ID isn't available and XPath is too slow, you reach for platform-specific selectors. UIAutomator2 on Android and XCUITest on iOS each expose a rich query language that is faster and more expressive than XPath. This lesson goes deep on both — the syntax, the patterns, and the edge cases that trip up beginners.

UIAutomator2 deep dive

UIAutomator2 selectors use the UiSelector class. You pass a string that is evaluated by the on-device UIAutomator2 engine, not by Appium. This means the query runs natively on the device, which is why it is fast.

Text matching

// Exact match (case-sensitive)
new UiSelector().text("Add to Cart")
 
// Case-insensitive match
new UiSelector().textMatches("(?i)add to cart")
 
// Substring match
new UiSelector().textContains("Cart")
 
// Regex match
new UiSelector().textMatches("^Add.*")

By class and index

Every Android widget has a class name derived from the Java view hierarchy:

// All EditText fields
new UiSelector().className("android.widget.EditText")
 
// The second EditText (0-indexed)
new UiSelector().className("android.widget.EditText").instance(1)
 
// Clickable ImageViews
new UiSelector().className("android.widget.ImageView").clickable(true)

Child selectors

Child selectors navigate parent-child relationships without XPath:

// A button inside a specific container
new UiSelector()
    .resourceId("com.example:id/checkout_form")
    .childSelector(
        new UiSelector().text("Confirm Order")
    )

Scrollable selectors

UiScrollable wraps a UiSelector that identifies the scrollable container:

// Scroll down a list until "Privacy Policy" is visible
new UiScrollable(new UiSelector().scrollable(true))
    .scrollIntoView(new UiSelector().text("Privacy Policy"))
 
// Scroll forward and backward
new UiScrollable(new UiSelector().scrollable(true))
    .scrollForward()   // one page down
 
new UiScrollable(new UiSelector().scrollable(true))
    .scrollBackward()  // one page up
 
// Scroll to the end
new UiScrollable(new UiSelector().scrollable(true))
    .scrollToEnd(10)   // max 10 swipes

In code:

driver.findElement(AppiumBy.androidUIAutomator(
    "new UiScrollable(new UiSelector().resourceId(\"com.example:id/product_list\"))" +
    ".scrollIntoView(new UiSelector().text(\"Samsung Galaxy S24\"))"
));

UiCollection

UiCollection targets a container and counts or iterates its children:

// Click the 3rd item in a list
new UiCollection(new UiSelector().className("android.widget.ListView"))
    .getChildByIndex(new UiSelector().className("android.widget.LinearLayout"), 2)

Common mistakes with UIAutomator2

Mismatched quotes. The selector is a Java string that contains Java code. Single quotes inside the selector are fine; double quotes must be escaped:

// Correct
AppiumBy.androidUIAutomator("new UiSelector().text(\"Sign In\")")
 
// Also correct — use JSON-style escaping
AppiumBy.androidUIAutomator("new UiSelector().text('Sign In')")

Wrong class name. Use Appium Inspector to verify the exact class — it is android.widget.Button, not Button or Widget.Button.

instance() is 0-based. instance(0) is the first match, not instance(1).

XCUITest deep dive

XCUITest locators run inside the WebDriverAgent on the iOS device or Simulator. There are two flavours: NSPredicate strings and Class Chains.

NSPredicate strings

NSPredicate is Apple's query language for collections. Appium exposes it through AppiumBy.iOSNsPredicateString().

// Exact match by label
AppiumBy.iOSNsPredicateString("label == 'Add to Cart'")
 
// Case-insensitive contains
AppiumBy.iOSNsPredicateString("label CONTAINS[c] 'cart'")
 
// By type and enabled state
AppiumBy.iOSNsPredicateString(
    "type == 'XCUIElementTypeButton' AND enabled == true"
)
 
// By value not empty
AppiumBy.iOSNsPredicateString(
    "type == 'XCUIElementTypeTextField' AND value != ''"
)
 
// Or condition
AppiumBy.iOSNsPredicateString(
    "label == 'OK' OR label == 'Allow'"
)

NSPredicate operators:

OperatorMeaning
==Equals
!=Not equals
CONTAINSContains substring
CONTAINS[c]Case-insensitive contains
BEGINSWITHStarts with
ENDSWITHEnds with
MATCHESRegex match
ANDLogical AND
ORLogical OR

XCUITest element types

Common types you will encounter:

TypeDescription
XCUIElementTypeButtonTappable buttons
XCUIElementTypeTextFieldSingle-line text input
XCUIElementTypeSecureTextFieldPassword field
XCUIElementTypeStaticTextLabels and text
XCUIElementTypeCellTable/collection view cells
XCUIElementTypeTableTable views
XCUIElementTypeNavigationBarNavigation bars
XCUIElementTypeAlertSystem and custom alerts

iOS class chains

Class chains are like a fast, strict XPath for XCUITest:

// All buttons in a navigation bar
AppiumBy.iOSClassChain("**/XCUIElementTypeNavigationBar/XCUIElementTypeButton")
 
// Specific button by index
AppiumBy.iOSClassChain("**/XCUIElementTypeNavigationBar/XCUIElementTypeButton[1]")
 
// Cell with a specific label, then its text field
AppiumBy.iOSClassChain(
    "**/XCUIElementTypeCell[`label == 'Email'`]/XCUIElementTypeTextField"
)
 
// Last element of type
AppiumBy.iOSClassChain("**/XCUIElementTypeButton[-1]")

Class chains are faster than NSPredicate strings when you need to navigate a hierarchy, because the engine traverses only the matched path rather than scanning all elements.

When to use which iOS selector

ScenarioStrategy
Element has accessibilityIdentifierAccessibility ID
Filter by multiple attributesNSPredicate string
Navigate parent-child hierarchyClass chain
Last resortXPath

Cross-platform page objects

When building Page Objects that work on both platforms, use conditional logic based on the driver type or a platform flag:

public WebElement getLoginButton() {
    if (driver instanceof AndroidDriver) {
        return driver.findElement(AppiumBy.id("com.example:id/login_btn"));
    } else {
        return driver.findElement(AppiumBy.accessibilityId("LoginButton"));
    }
}

Or, better, make both platforms use Accessibility ID from the start — it is the one strategy that unifies the two.

// tip to track lessons you complete and pick up where you left off across devices.