Craig Grummitt – Before I forget

Discoveries in Swift and iOS development.

,

App Screenshots – stop doing them manually!

·

This week I made a significant update to Subtitles Viewer (you can check it out here).

Now updating screenshots for the App Store for an update is often not such a big deal, but Subtitles Viewer has 22 different App Store localizations, which means if I have 4 screenshots, that’s 22×4=88 screenshots! Oh – and then there’s a pro version of the app which may have slight differences in the screenshots, which means there are double that – 176 screenshots, to capture, add some marketing materials, and then upload to the App Store in each of the localizations…

All of this can get quite time-consuming!

Enter fastlane, automating such tasks. Just tell it which screens you want, on which devices, in which languages – then go make a cup of coffee!

This is the first of three posts. Today we’ll capture the actual app screens. Next time we’ll dress them up with marketing frames and captions. After that, we’ll upload the whole lot to App Store Connect without clicking through 22 localization tabs by hand.

What we’re actually automating

fastlane has a tool called Snapshot. The idea is almost embarrassingly simple. You write a little UI test – the same kind of test Xcode already knows how to run – except instead of checking that a button exists, you say “take a picture here”.

fastlane then boots the simulator, launches your app, walks through those spots, and saves a PNG. Then it switches language and does it again. Then it switches device and does it all over again.

You’re not taking 176 screenshots. You’re describing one happy path through the app, and letting the computer repeat it.

Getting fastlane into the project

If you haven’t used fastlane before, the least painful way is to add it with Bundler, so the version is stored in the project. Future-you will thank present-you.

In the folder that contains your Xcode project, create a Gemfile (or add this line to one you already have):

source "https://rubygems.org"

gem "fastlane"

Then:

bundle install
bundle exec fastlane snapshot init

That gives you two useful things:

  • Snapfile – the shopping list of devices and languages
  • SnapshotHelper.swift file – a little bit of glue that lets your UI test talk to fastlane

Add SnapshotHelper.swift to your UI test target in Xcode. If you don’t have a UI test target yet, add one with File → New → Target → UI Testing Bundle.

The shopping list

Open fastlane/Snapfile. This is where you say what to capture. Mine for Subtitles Viewer looks roughly like this (trimmed a little):

devices([
  "iPhone 17 Pro Max",
  "iPad Pro 13-inch (M5)"
])

languages([
  "en-US",
  "es-ES",
  "fr-FR",
  "de-DE",
  "ja",
  "ko"
  # ...and the rest of the store localizations
])

scheme("SubtitlesViewer")
output_directory("./fastlane/screenshots")
override_status_bar(true)

A few notes from doing this for real:

  • Device names have to match simulators you actually have installed. Open Xcode, look at the simulator list, and copy the names exactly. I used the current Pro Max and 13-inch iPad because those fill Apple’s 6.9-inch and 13-inch screenshot slots.
  • You do not need to start with 22 languages. Get English working on one device first. Then add the rest.
  • override_status_bar(true) is a small thing that makes a big difference. It sets the clock to 9:41, full battery, full wifi – that classic Apple marketing look, instead of the reality of working at 10pm on a Saturday – no-one needs to know that!

A tiny test that takes the pictures

This is the heart of it. In your UI test target, write a test that launches the app, waits until a screen looks right, and calls snapshot:

func testScreenshots() throws {
    let app = XCUIApplication()
    setupSnapshot(app)
    app.launchArguments += [
        "-UITestScreenshots",
        "-UITestSkipIntro"
    ]
    app.launch()

    XCTAssertTrue(app.otherElements["searchResults"].waitForExistence(timeout: 8))
    snapshot("01Search")

    app.tabBars.buttons["Language"].tap()
    XCTAssertTrue(app.otherElements["languageList"].waitForExistence(timeout: 5))
    snapshot("02Language")
}

setupSnapshot(app) is from the helper file. It tells the app which language fastlane wants this time. snapshot("01Search") is the actual shutter click. The name you pass in becomes the filename, so I number them – 0102 – so they show up on the App Store in the order I want.

You’ll have a much nicer time if you give important views accessibility identifiers in the app itself (searchResultslanguageList, and so on). Then the test can wait for those, instead of guessing at button titles that change with every language.

Make the screens look like a store listing

A screenshot of your real app, five seconds after launch, is rarely what you want on the store. There might be an onboarding carousel. An empty library. A network spinner. A search box with nothing in it.

The trick is to give the app a “screenshot mode”. Those launch arguments above are just strings your app can look for when it starts. In Subtitles Viewer I used them to:

  • skip the intro animation
  • stop talking to the real subtitle server, and return a canned search result instead – so a movie poster always appears, even if the network is having a day
  • pre-select the right subtitle language for the store localization we’re capturing

That last one is the fun part. When the App Store page is in Japanese, you don’t want the screenshot searching for Back to the Future. You want Spirited Away. Korean gets Parasite. French gets Amélie. The UI test looks at the language fastlane asked for, and passes a well-known film for that language into the app.

Same screens, same layout – but each localization feels like it was made for that store page, because it was.

Hit go

From the project folder:

bundle exec fastlane snapshot

The first run takes a while. The simulator boots, the app launches, a screenshot is taken, then it switches language and does it all again. For Subtitles Viewer that’s a pile of languages × two devices × two screens. I make coffee. Then I had a folder full of screenshots:

fastlane/screenshots/en-US/iPhone 17 Pro Max-01Search.png
fastlane/screenshots/en-US/iPhone 17 Pro Max-02Language.png
fastlane/screenshots/ja/iPhone 17 Pro Max-01Search.png
...

fastlane also writes a little HTML gallery. Open it in a browser and skim every language. That’s how you catch the one locale where a long button title wrapped onto three lines and covered the poster.

A few things I learned the hard way

You don’t need a unique capture for every App Store localization. English (Australia) looks the same as English (US) in this app, so I captured en-US once and reused it for en-AU. Same idea for Spanish (Mexico), French (Canada), and Portuguese (Brazil). That’s how 18 captures cover 22 store listings.

Wait for the thing you actually want to photograph. If you call snapshot before the poster has loaded, you’ll get a beautiful shot of a spinner. Wait for the real content – then click the shutter.

Don’t throw away good screenshots because one language failed. Leave existing files in place and recapture only what’s missing. When you’re doing this many locales, something will flake out once. That’s fine. Run it again.

Get one screen, one language, one device perfect first. It’s tempting to paste all 22 locales into the Snapfile on day one. Resist. A working English iPhone screenshot teaches you more than a two-hour run that fails on the 14th language.

What we have (and what we don’t, yet)

What we have now are honest screenshots of the app, in every language, on iPhone and iPad. That’s already a huge amount of manual work gone.

They’re not yet the framed, captioned images you see on the store – “Subtitles at home!”, “Subtitles at the cinema!”. Those marketing shots are a second pass on top of these PNGs.

That’s part 2.

Part 3 is the bit I used to dread: uploading 176 images into App Store Connect. Spoiler: fastlane can do that too.


Comments

Leave a Reply

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