When building an UI test, you often need to automatically close the visible modal or pop the view currently stacked. If you use the XCTest framework, then you have to only rely on what is visible on the screen (as if you were an user).

Close buttons in navigation bar

Close button in quick look modal

Usual way

Let’s say you have a button inside a SwiftUI view.

1
2
3
4
Button("Close") {
	// close screen
}
.accessibility(identifier: "close-button")

Accessing the button from your test could be:

1
2
3
let button: XCUIElement = self.app.buttons.matching(identifier: "close-button").element
XCTAssert(button.exists)
button.tap()

However, it’s not always easy or possible, for instance:

  • A view managed by iOS itself (e.g. camera picker, quick look, etc).
  • A navigation stack with SwiftUI.

Depending on the context, we might find a more or less clean way to achieve this goal. For instance, you could:

  • Rely on the element name (e.g. app.buttons["Back"]). However it’s edgy when your app is localized, even more if it’s an element managed by the system.
  • Rely on the element accessibility identifier that you set yourself (as above). But you can’t always access the element from your code.

Suggested approach

  1. We look for the visible navigation bar. If you take SwiftUI, the view hierarchy will probably contain closed screen, so you can’t rely on the last view in the stack.
  2. We assume that the “close button” is the first one in the navigation bar.
  3. We wait for this button to be “exist”.
  4. We tap the button.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func closeVisibleScreen() -> Void {
            
    for navBar in self.app.navigationBars {
        if navBar.isVisible == true { // 1
            let closeButton: XCUIElement = navBar.buttons.firstMatch // 2
            XCTAssert(closeButton.waitForExistence(timeout: 1)) // 3
            closeButton.tap() // 4
            break
        }
    }
    
}

Final note: I experienced a case where no navigation bar was “visible”. It may be the case when a modal is being dismissed (or just finished being dismissed). You may have to add a small sleep or retry logic to make the logic more robust.