Image of founer

Generating Screenshots for iOS with Fastlane

February 13, 2020

Fastlane is a very useful tool for automating everything around app development for iOS and Android. It works really well with React Native as well, altough it is not very well documented. Therefor this is supposed to be a "complete" guide for generating screenshots with fastlane for iOS. Please feel free to contact me if something is missing.

The guide will assume that you have fastlane already installed and initialized in your project.

Setting up fastlane

Start by initializing snapshot in firebase. This step only creates a snapfile and Snapshothelper.swift in your fastlane folder.

fastlane snapshot init

The snapfile will look have some of the fields below. This is however the configuration I prefer.

# A list of devices you want to take the screenshots from
devices([
  "iPhone 11 Pro Max",
  "iPhone 8 Plus",
])

languages([
  "sv",
  "en-US"
])

# The name of the scheme which contains the UI Tests
scheme("UITestScheme")

# Where should the resulting screenshots be stored?
output_directory("./fastlane/screenshots")

# remove the '#' to clear all previously generated screenshots before creating new ones
clear_previous_screenshots(true)

workspace('./ios/AppName.xcworkspace')

skip_open_summary(true)

derived_data_path('./fastlane/derived')

Finally we have to add the new lanes for generating and uploading the screenshots to appstoreconnect.

 desc 'Generate screenshots for app store'
  lane :generateScreenshots do
    capture_screenshots(
      reinstall_app: true
    )
  end

  desc 'Upload metadata (screenshots) to app store'
  lane :uploadScreenshots do
    deliver(
    force: true,
    skip_metadata: true,
    skip_screenshots: false,
    skip_binary_upload: true,
    username: "example@email.com"
  )

The last argument reinstall_app is not necessary. It does however increase the consistency of your screenshots. If you for example use the app in testting and set some values, then those values could be used when generating your screenshots.

Setting up Xcode

You also have to add a new target UI testing target in Xcode:

File->New->Target...->UI Testing Bundle

The snapshot init command added a file named SnapshotHelper.swift. Add this to your newly created target.

You will also need a new scheme for running the tests. Create it and change the following settings:

  • Under the build tab, make sure that both test and run are checked.
  • Disable parallelize builds
  • Under the run tab, change configuration to release

Changes to React Native code

To be able to interact with elements on the screen they must have an ID that Xcode can find. Set the prop testID on all elements that you want to interact with in any way.

<Button testID="someIdThatIsUniqueInTheView"></Button>

You can also use dynamic ID:s

<Button testID={key}></Button>

Things to note:

  • Don't set the testID on the root element of a view. It will fail.
  • Screenshots for android requires another prop AccessibilityLabel

Swift code for the tests

To specify what screens you want to capture you have to write UI Tests in swift. There already exists a base file which we will modify in your new target.

It can look somoething like this:

import XCTest

class MyUITests: XCTestCase {
  
  override func setUp() {
    
    continueAfterFailure = false
    
  }
  
  override func tearDown() {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
  }
  
  func testExample() {
  
  }
}

The function testExample is the starting point for your test. You must start by setting up the app with snapshot

func testExample() {
    let app = XCUIApplication()
    setupSnapshot(app)
    app.launch()
    
    ...
}

To select a Reacct Native element use:

let element = app.otherElements["myTestID"]

The most important funciton, capture a screenshot:

snapshot("01name")

Some useful actions you can preform on an element:

// Wait for the element to render
let exists = element.waitForExistence(timeout: 10)

// check if element exists
if (element.exists) {
	// It exists!
}

// Tap en element
element.tap()

// Swipe an element
element.swipeLeft()
element.swipeRight()
element.swipeDown()
element.swipeUp()

// Enter text
element.typeText("Some text")

If you need to handle alerts that your app displays you can use:

app.alerts["the title of your alert"].buttons["The text in the button"].tap()