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; }