When writing any UI Test automation it is important to interact with elements on the screen only when they are ready to be interacted with. When navigating to a new screen, you are not guaranteed that all of the screen elements exist and are hittable the very second that test code is executed.
This post explores techniques for checking if the minimum set of elements are ready for interaction as well as a way to determine that things like activity indicators (aka spinners) are not active.
Uses
- Checking that activity indicators or other initial alerts, tool tips, and modals are dismissed is important in all cases. My opinion is that this is vital for any screen where I expect that those behaviors will occur.
- Checking that all elements exist on a screen is an optional step to run all of the time either from a
screenReady()
function or within an assertion at start of a test. - Use with set of tests that only validate that navigation works and screens get into initial expected state.
- Even if you would use this type of function infrequently, it is a forcing function to implement element queries. Best practices then can be applied so that accessibility identifiers exist for each element.
After working through a process where I always check that every element exists before interacting with a screen, I have come to the conclusion that sometimes it is okay to skip this comprehensive step and only check for existence right before an element is used. Always running an elementsReady()
function takes added time and means that if an element’s query requires a change then all tests which call elementsReady()
will fail. Finally, depending on my schedule constraints, I may want to defer the definition of screen elements which are not needed for test(s) being implemented. Sometimes, the process of defining a screen element will grow into making modifications to the app source code to add accessibility identifiers.
Below, I describe a few examples of code that check the state of screen elements. Each example has pros and cons making them suitable for some projects and not for others depending on your requirements.
Screen Ready
Wait for Blocking Activity to Complete
Below is an example for code I would use to check that an activity indicator is no longer present for a screen. Not shown are ways to deal with modals, alerts and tool tips.
func screenReady(_ timeout: TimeInterval = 5) -> Bool {
let myExpectation = XCTNSPredicateExpectation(predicate:
NSPredicate(format: "exists == false"), object: activityIndicator)
let result = XCTWaiter().wait(for: [myExpectation], timeout:
timeout)
return result == .completed
}
Below, I describe a few examples of code that check the state of screen elements. Each example has pros and cons making them suitable for some projects and not for others depending on your requirements. For the first two methods, asserts are included for brevity in the examples.
Method 1 – XCUIElement’s exist
property
func elementsReadyUsingExists() -> Bool {
var result = false
XCTContext.runActivity(named: "STEP - Elements Ready by `exists` property") { _ in
result = labelStaticText.exists && openSafariButton.exists && showLabelButton.exists && && missingButton.exists && openSettingsButton.exists
}
XCTAssertTrue(outcome, "All elements do not exist using `exists` property")
return result
}
Method 2 – XCUIElement’s waitForExistence
function
func elementsReadyUsingWaitForExpectations(timeout: TimeInterval = 5) -> Bool {
XCTContext.runActivity(named: "STEP - Elements Ready by `waitForExistence` looping through element array") { _ in
let elements: [XCUIElement] = [labelStaticText, openSafariButton, showLabelButton, missingButton, openSettingsButton]
for element in elements {
XCTAssertTrue(element.waitForExistence(timeout: timeout), "element named \(element.description) does not exist with in \(timeout) seconds")
}
}
return true
}
Method 3 – XCTWaiter to wait for all elements at one time
func elementsReadyUsingXCTWaiter(timeout: TimeInterval = 5) -> Bool {
var result:XCTWaiter.Result = .timedOut
XCTContext.runActivity(named: "STEP - Elements Ready by `XCTWaiter.wait()") { _ in
let elements: [XCUIElement] = [labelStaticText, openSafariButton, showLabelButton, missingButton, openSettingsButton]
let expectations = elements.map { XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == true"), object: $0) }
result = XCTWaiter().wait(for: expectations, timeout: timeout)
}
return result == .completed
}
// Assertion used when calling elementsReadyUsingXCTWaiter() from a test case
// XCTAssertTrue(demoScreen.elementsReadyUsingXCTWaiter(timeout: 5), "All elements do not exist (XCTWaiter method)")
Performance and Trade Offs
Performance data is also shown for a skeletal app I use for demos. It only has one screen containing 4 elements.
The data below is based on checking for existence of 4 elements which do exist and 1 element which has a query, but does not exist. Queries are only evaluated when used, so defining a dummy element is not a problem.
The logging data shown below the performance summary is for each method and first, in text, what the assertion portion of the log file looks like and then in the image, what the Test Report Navigator looks like, fully expanded.
Approach | Timeout = 0 | Timeout = 5 | Timeout = 30 |
exists property | 0.06 | n/a | n/a |
check 1 at time | 0.06 | 8.1 | 33.11 |
check all at once | 0.07 | 5.09 | 30 |
t = 5.14s STEP - Elements Ready by `exists` property
t = 5.21s Assertion Failure: DemoScreen.swift:49: XCTAssertTrue failed - All elements do not exist

Method 1 - Check using exist
property with no timeout
t = 38.17s Checking `Expect predicate `exists == 1` for object "missing-element" Button`
t = 38.27s Assertion Failure: DemoScreen.swift:65: XCTAssertTrue failed - element named "missing-element" Button does not exist with in 30.0 seconds

Method 2 - Check each element, one at a time
t = 5.04s STEP - Elements Ready by `XCTWaiter.wait()
...
t = 34.91s Checking `Expect predicate `exists == 1` for object "missing-element" Button`
t = 35.04s Assertion Failure: DemoAutoUITests.swift:61: XCTAssertTrue failed - All elements do not exist

Method 3 - Check all elements at once
Method 1 is as fast as possible to check that all elements expected to exist on loading a screen exist. Its disadvantage is that neither by looking at the log or the Test Navigator’s report is it possible to see which particular element does not exist.
Method 2 is slightly slower than other approaches. Its disadvantage is that you will always have extra checking greater than the timeout. Its advantage is that the assertion message shows exactly which element does not exist. There is no need to look in more detail at earlier steps in the test run.
Method 3 is as fast as Method 1. Its advantage over Method 2 is speed, though it is necessary to either expand an earlier step in the Test Navigator report or look at line before the assertion in the test log.
My preference is Method 2 since it shows exactly what the issue is only by looking at the assertion message. This makes it possible to show only the assertions emitted by the failed test cases without a large time penalty.
The case can be made that Method 3 is best since speed of test runs, especially as a test suite grows to tens or hundreds of cases is crucial for continuous integration pipelines. You could also argue that a test should fail infrequently, so test run time is more important than best clarity in assertion message.
Thank you for reading. Please leave comments and questions for this post. I check for them regularly.