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 swipesIn 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:
| Operator | Meaning |
|---|---|
== | Equals |
!= | Not equals |
CONTAINS | Contains substring |
CONTAINS[c] | Case-insensitive contains |
BEGINSWITH | Starts with |
ENDSWITH | Ends with |
MATCHES | Regex match |
AND | Logical AND |
OR | Logical OR |
XCUITest element types
Common types you will encounter:
| Type | Description |
|---|---|
XCUIElementTypeButton | Tappable buttons |
XCUIElementTypeTextField | Single-line text input |
XCUIElementTypeSecureTextField | Password field |
XCUIElementTypeStaticText | Labels and text |
XCUIElementTypeCell | Table/collection view cells |
XCUIElementTypeTable | Table views |
XCUIElementTypeNavigationBar | Navigation bars |
XCUIElementTypeAlert | System 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
| Scenario | Strategy |
|---|---|
| Element has accessibilityIdentifier | Accessibility ID |
| Filter by multiple attributes | NSPredicate string |
| Navigate parent-child hierarchy | Class chain |
| Last resort | XPath |
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.