How to tap a cell in a UITable/CollectionView that isn't on screen yet in iOS UI Testing?

Our app has some UITableViews and UICollectionViews where we need to tap cells that, depending on the device, might not be on screen (think a "View All" button). Unfortunately, simply trying to do a swipeUp() on the view can just **** past the cell we need to tap. Is there an official way to get the view to scroll to the thing we need without potentially blowing past it?

Replies

The only way I know of to do this with any reliability is as shown in the following psuedo-code


//use press instead of swipeUp to control scroll rate (adjust dy values as needed for your purpose)
var cells = (query for the cells you're interested in)
//if UITableView, you should be able to do a query to get all cells, even off-screen
//however, UICollectionViews have to be requeried with every swipe to catch new elements that come into scope
var cellExists = (check if your cell exists and isHittable)
while (cellExists == false) {
     let topCoordinate = referenceToScrollViewElement.coordinate(withNormalizedOffset: CGVector(dx: 0.0, dy: 0.25)   
     let bottomCoordinate = referenceToScrollViewElement.coordinate(withNormalizedOffset: CGVector(dx: 0.0, dy: 0.75))
     bottomCoordinate.press(forDuration: 0.0, thenDragTo: topCoordinate)
     //if you're sarching through a UICollectionView, re-query for the currently available 'cells' which may have changed as a result operf swiping
     cellExists = 
}

Converted to Swift 5.1 code


extension XCUIElement {

    /// Call this on an `XCUIElement` with scrolling capabilities like a table view or scroll view.
    /// Use this method when trying to tap a cell that is known to be offscreen, and therefore
    /// not necessarily dequeued and created yet.
    /// - Parameter query: A query for an element that doesn't necessarily exist yet.
    func scrollIntoExistence(query: XCUIElementQuery) -> Bool {
        // swiftlint:disable empty_count
        for _ in (0..<10) {
            let startCoord = coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0.5))
            let endCoord = startCoord.withOffset(CGVector(dx: 0.0, dy: -100))
            startCoord.press(forDuration: 0.01, thenDragTo: endCoord)
            if query.count > 0 { return true } // We successfully scrolled the element into existence
        }
        Thread.sleep(forTimeInterval: 2)
        return query.count > 0
        // swiftlint:enable empty_count
    }
}

Changing the while loop to a countdown to eventually expire