Make Fastlane Snapshot Work with React Native

Using fastlane snapshot on react native app requires quite some efforts

Update 02/10/2021: for the Android version, i.e. fastlane screengrab, we have shared our experience in Make Fastlane Screengrab Work with React Native.

We recently submitted an app made in react native to Apple App Store. The whole process was highly complex, but with the help of fastlane, it was bumpy but still manageable…until we started to generate screenshots of the app. Screenshots are required by Apple App Store for two sizes of iPhone (6.5-inch and 5.5-inch) and two versions of iPad Pro (12.9-inch 2nd and 3rd generation). Although screenshots can be produced manually, it is not a scalable solution once the numbers of device types and supported languages increase. Hence, automated screenshot generation is unavoidable.

fastlane offers a command snapshot to automatically take screenshots of an app, leveraging the UITest capability of Xcode. Unfortunately, we encountered quite a few issues when going through the doc of fastlane snapshot. We would like to share our experience below and show our current solutions that either resolve or work around the issues.

“Timestamped Event Matching Error”

We followed the tutorial recommended in fastlane snapshot doc, but once the UITest started to record, tapping a button either was not recorded or faced with a“Timestamped Event Matching Error”. Often times Xcode would even hang and not respond when recording was ended. Google and SO search seemed to suggest that the problem was due to not having provided testID to the button component. We proceeded to add testID to the buttons, yet the problem persisted. It seemed to us that no matter what we did, we simply could not tame the beast of Xcode to record any app actions in UITest.

Fortunately, we stumbled upon this repo, which showed that by adding testID to TouchableHighlight, Xcode UITest was able to recognize TouchableHighlight and execute actions associated with it. Since all our buttons were written using the react-native-elements library, we didn’t have a TouchableHighlight laying around. So we created one for testing purpose. While tapping it still wasn’t recorded by UITest in Xcode, fastlane snapshot was able to call XCUIApplication().otherElements[‘some_test_id’].tap() defined in xxx_appUITests.swift. This was a breakthrough! At least, we found a way to trigger something in the app during fastlane snapshot.

We also found that one of the comments in an issue on fastlane mentioned that they used dummy buttons to trigger redux state changes in order to take desired screenshots. This seemed to us a very hacky workaround and initially we didn’t want to pursue it. Yet, now it was the only option left. So we added a few dummy TouchableHighlight buttons with unique testID, set up their callbacks to change the screen to desired states, and viola, fastlane snapshot worked as expected.

Hide The Dummy Buttons

The dummy button hack worked. The next problem was to hide them when screenshot was taken. Since the screenshot was taken programmatically, we thought the dummy buttons could be easily hidden using any of the following approaches:

  1. Not providing text in the dummy button, something like this
<View>
<TouchableHighlight
onPress={() => callback()}
testID="some_test_id">
<Text></Text>
</TouchableHighlight>
</View>

2. Place the dummy button behind the main screen via z-index trick

3. Make the dummy button text transparent.

However, it turned out approach 1 and 2 didn’t work, because when configured like that, the dummy buttons became untappable (i.e., they were too hidden). Only approach 3 worked, provided that another tweak was also made: the dummy buttons must have the style {position: 'absolute'} to elevate them above everything else. Otherwise, they would occupy space on the screen and mess up the layout of other visible components.

Remove The Dummy Buttons in Production

Approach 3 successfully hid the dummy buttons during screenshots, but if we released the app like that, those dummy buttons would be exposed to users. Thus, we wanted to remove the dummy buttons in the production release. This required that the rendering of the dummy buttons to be contingent on some environment variable, e.g. the env for screenshot was not production:

This approach worked. Now the production release did not contain the dummy buttons. However, it led to another problem: we had to take screenshots under dev instead of prod. If there is no difference between the dev and prod versions of the app, it is not an issue. However, if there is difference between the two versions, e.g. some immature features exist in dev but not ready for prod, one will have to mock the dev version as prod, while at the same time also specify that screenshot is available.

Unfortunately, our app fell in the second scenario, and we had to create a new environment variable screenshot to determine whether the dummy buttons should be rendered (i.e. instead of checking if (env === 'prod'), we checked if (screenshot === 'true')) and use env=prod for the development environment temporarily. Furthermore, since the screenshots were taken under dev, we had to also disable yellow box warning when screenshot was available.

This rather convoluted approach worked. We were able to take screenshots with the dummy buttons present (but invisible and not affecting the app layout) and only showing the production ready features.

Avoid Environment Variables Cache

Since we made changes from env=dev to env=prod in one of our env files, we must run npx react-native start --reset-cache manually to load such change when metro bundler was started. Otherwise, the previously cached environment variables would be used, irrespective of the change in the env file.

To automate this workflow in Fastfile, we relied on tmux to start the cache-free metro bundler before calling fastlane snapshot.

Wrong Screenshot Dimension

Everything seemed to be working now. We were able to use undetectable dummy buttons to automatically generate screenshots and remove the dummy buttons in production release. The next hurdle was that the dimension of the raw screenshot did not match the requirement specified by Apple App Store. Dimension of screenshot has been an issue for fastlane in the past, and it should be resolved by using fastlane frameit. fastlane frameit puts a device frame surrounding a raw screenshot and allows for additional marketing text on top or bottom of the framed screenshot. In our case, the output of fastlane frameit seemed to meet the dimension requirements, but just to be safe, we also implemented a script to guarantee that the final screenshot output matched the dimension requirement perfectly:

This script required installation of imagemagick. It can be used like this: DEVICE="iPhone 11" resize_screenshot.sh

Missing iPad Pro (12.9-inch) (2nd generation) frame

The final obstacle was the missing iPad Pro (12.9-inch)(2nd generation) frame. Screenshots for iPad Pro (12.9-inch)(2nd generation) is required by Apple App Store, yet its frame was mysteriously missing in the latest frame folder. After some digging, we found out that the frame name for iPad Pro (12.9-inch)(2nd generation) is actually Apple iPad Pro and it lives in the 1572609390 folder .

We downloaded the “Apple iPad Pro Space Grey” frame to ~/.fastlane/frameit/latest/ and the problem was resolved.

Summary

Taking screenshots is a required step before releasing an app to Apple App Store. Although doing it manually is feasible at the beginning, it is not a scalable approach once the numbers of device types and supported languages grow in the future. fastlane snapshot provides an automated way to generate screenshots programmatically, yet we have found that this tool does not work very well with react native apps out of the box. In this simple write up, we have documented our struggles with fastlane snapshot, and provided our solution/work-arounds to smooth out all the kinks. We hope this write up will help some poor soul who is shaking his monitor right now and asking the ultimate question in the universe: why is this not working?! (we do not deny nor admit that we have done the same thing)

Hi, I am from the Earth. And you?