More XCTest UT Testing: A Helper function to scroll to a cell in a UICollectionView

At Luma Touch, we continue to use (and highly recommend) fastlane (now a part of fabric.io tools) to create and upload builds, manage App Store metadata, and most importantly, create and upload screenshots to the App Store.

The most difficult part of creating screenshots is setting up the XCTest automated tests in XCode to get everything in the right ‘place’ for each screenshot.  My previous post discussed a number of hints and tips and showed a complete example of how to do this.

For our next app, a large part of the screenshot setup requires selecting effect presets from a collection view, and scrolling the collection view to the correct location.  This is made particularly difficult since each device has a different sizes for the collection views and different sizes for the cells within them.  After a number of tries at guessing the right distance to ‘swipe’ with coordinates that would work for all devices, I ended up writing a new helper function that would automate scrolling to a particular cell within a collectionView.

The scrollTo Helper Function

The function looks like this (full source at the bottom of this post):

func scrollTo(cellIdentifier: String, collectionViewElement: XCUIElement, scrollUp: Bool = false, fullyVisible: Bool = false) -> Bool

To use it in your test code, your UICollectionView and each cell should have its accessibilityIdentifier set in code.  I usually include the section and item in the accessibilityIdentifier for the cell (i.e.: “presetcell-0-20”).  So, to scroll to a cell in a collectionView with an accessibilityIdentifier of “presets”, you could use:

scrollTo("presetcell-0-20", collectionViewElement: app.collectionViews["presets"])

This will scroll downward through the collectionView until “presetcell-0-20” is hittable.  If you wanted to make sure that the cell is fully visible (for a screenshot for example), you would use:

scrollTo("presetcell-0-20", collectionViewElement: app.collectionViews["presets"], scrollUp: false, fullyVisible: true)

And, finally, if you want to scroll upwards to “presetcell-0-5” and make it fully visible, you would use:

scrollTo(“presetcell-0-5”, collectionViewElement: app.collectionViews[“presets”], scrollUp: true, fullyVisible: true)

An Important Note About Cell accessibilityIdentifiers

In my previous post, I noted how I cleared the accessibilityIdentifier in tableView:didEndDisplayingCell.  However, this has a small chance of confusing the scrollTo helper function, so I’ve changed to setting the accessibilityIdentifier uniquely in didEndDisplayingCell like this:

cell.accessibilityIdentifier = [NSString stringWithFormat:@"notvisible-%ld-%ld", (long)indexPath.section, (long)indexPath.item];

How The Helper Function Works

The helper function scrolls 1/2 the height of the collection View over and over until either the cell you’re looking for is hittable, or the scroll doesn’t actually change anything (you’ve hit the top or bottom of the collection view).

Note that the first check it does for the cell, looks like this:

collectionViewElement.cells.matchingIdentifier(cellIdentifier).count > 0

This lets you query the collectionView cells to see if the identifier is present without having the test fail by directly checking for the cell with collectionViewElement.cells[cellIdentifier], you would get a failure in the test, and it wouldn’t continue.

The code checks to see if the touch changed anything by keeping track of the ‘middle’ cell in the list of cells for the collection view (which by-the-way, might include cells that are no longer displayed), and determines if it’s the same cell id and same frame to see if anything has changed.

After the cell is found to be hittable, if you want it fully visible, there is a second loop that scrolls up or down by a smaller amount (1/2 the height of the cell) until the cell’s frame is fully contained by the collection view’s frame.

The Helper Function Source

    func scrollTo(cellIdentifier: String, collectionViewElement: XCUIElement, scrollUp: Bool = false, fullyVisible: Bool = false) -> Bool {
        var rtn = false
        var lastMidCellID = ""
        var lastMidCellRect = CGRectZero

        var currentMidCell = collectionViewElement.cells.elementBoundByIndex(collectionViewElement.cells.count / 2)

        // Scroll until the requested cell is hittable, or until we try and scroll but nothing changes

        while (lastMidCellID != currentMidCell.identifier || !CGRectEqualToRect(lastMidCellRect, currentMidCell.frame)) {            

            if (collectionViewElement.cells.matchingIdentifier(cellIdentifier).count > 0 && collectionViewElement.cells[cellIdentifier].exists && collectionViewElement.cells[cellIdentifier].hittable) {
                rtn = true
                break;
            }

            lastMidCellID = currentMidCell.identifier
            lastMidCellRect = currentMidCell.frame      // Need to capture this before the scroll

            if (scrollUp) {
                collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.4)).pressForDuration(0.01, thenDragToCoordinate: collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.9)))\
            }
            else {
                collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.9)).pressForDuration(0.01, thenDragToCoordinate: collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.4)))
            }

            currentMidCell = collectionViewElement.cells.elementBoundByIndex(collectionViewElement.cells.count / 2)
        }


        // If we want cell fully visible, do finer scrolling (1/2 height of cell relative to collection view) until cell frame fully contained by collection view frame

        if (fullyVisible) {
            let cell = collectionViewElement.cells[cellIdentifier]
            let scrollDistance = (cell.frame.height / 2) / collectionViewElement.frame.height

            while (!CGRectContainsRect(collectionViewElement.frame, cell.frame)) {   
                if (cell.frame.minY < collectionViewElement.frame.minY) {
                    collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.5)).pressForDuration(0.01, thenDragToCoordinate: collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.5 + scrollDistance)))
                }
                else {
                    collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.5)).pressForDuration(0.01, thenDragToCoordinate: collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.5 - scrollDistance)))
                }
            }
        }

        return rtn;
    }

 

Leave a Reply

Your email address will not be published. Required fields are marked *