My App Store Screenshots Flow

Creating 975 screenshots for the App Store is a daunting task if done manually. For my app, Quiet, I need screenshots for multiple devices and languages, and framing them nicely with labels adds even more work. Doing this by hand is simply not feasible. Here’s how I automated the process to save time and effort.

The Scale of the Task

Here’s the breakdown of the screenshot requirements:

Manually capturing, naming, and framing these would take hours and risk repetitive strain injuries. Automation is the solution.

Step 1: Writing UI Tests for Screenshots

The first step is to write UI tests to capture screenshots automatically. I created a Swift function to handle this.

func takeScreenshot(name: String) {
	let fullScreenshot = XCUIScreen.main.screenshot()
	let type = "public.png"
	let payload = fullScreenshot.pngRepresentation
	let name = "\(UIDevice.current.name)-\(name).png"
	let screenshot = XCTAttachment(
		uniformTypeIdentifier: type, 
		name: name, 
		payload: payload, 
		userInfo: nil
	)
	screenshot.lifetime = .keepAlways
	add(screenshot)
}

For iPhones, I wrote a test that navigates through the app’s tabs and takes screenshots. The iPad version follows a similar approach.

if UIDevice.current.userInterfaceIdiom == .phone {
	let app = XCUIApplication()
	app.launchArguments = ["enable-screenshot-data"]
	app.launch()

	let tabBar = app.tabBars.element(boundBy: 0)

	// First view
	takeScreenshot(name: "0")
	sleep(2)

	// Second view
	tabBar.buttons.element(boundBy: 1).tap()
	sleep(2)
	takeScreenshot(name: "1")
	sleep(2)

	// Third view
	tabBar.buttons.element(boundBy: 2).tap()
	sleep(2)
	takeScreenshot(name: "2")
	sleep(2)

	// Fourth view
	tabBar.buttons.element(boundBy: 3).tap()
	sleep(2)
	takeScreenshot(name: "3")
	sleep(2)
}

Xcode’s test recording feature simplifies this process by letting you navigate the app and generate UI test code automatically.

Step 2: Running UI Tests Automatically

To run the tests across all devices and languages, I adapted a shell script. It boots simulators, sets up the status bar, and runs the tests for each combination.

#!/bin/bash

# Xcode project path
projectName="/path/to/project/AppName/AppName.xcodeproj"

# Scheme for UI tests
schemeName="AppNameUITests"

# Temporary data folder
tempFolder="/tmp/AppNameData"

# Output folder for screenshots
targetFolder="/path/to/project/AppName/fastlane/screenshots/ios"

# Simulators to use
simulators=(
	"iPhone 11 Pro Max"
	"iPhone 14 Pro Max"
	"iPhone 8 Plus"
	"iPad Pro (12.9-inch) (6th generation)"
	"iPad Pro (12.9-inch) (2nd generation)"
)

# Languages (ISO 3166-1 codes)
languages=(
	"ar-SA"
	"de-DE"
	"en-GB"
	"es-ES"
	"es-MX"
	"fr-CA"
	"fr-FR"
	"he"
	"ja"
	"ko"
	"nl-NL"
	"pt-BR"
	"pt-PT"
	"zh-Hans"
	"zh-Hant"
)

for simulator in "${simulators[@]}"
do
	echo "$simulator booting"
	xcrun simctl boot "$simulator"

	# Get simulator UUID
	deviceUUID=$(xcrun simctl list devices | grep "(Booted)" | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})")
	echo "uuid: $deviceUUID"

	# Set status bar
	echo "Override statusbar"
	xcrun simctl status_bar $deviceUUID override --time "2007-01-09T09:41:00+01:00" --dataNetwork wifi --wifiMode active --wifiBars 3 --cellularMode active --cellularBars 4 --batteryState charged --batteryLevel 100 &> /dev/null

	for language in "${languages[@]}"
	do
		rm -rf $tempFolder/Logs/Test
		echo "Building and Running for $simulator in $language"
		xcodebuild -testLanguage $language -scheme $schemeName -project $projectName -derivedDataPath $tempFolder -destination "platform=iOS Simulator,name=$simulator" build test
		echo "Collecting Results..."
		mkdir -p "$targetFolder/$language"
		find $tempFolder/Logs/Test -maxdepth 1 -type d -exec xcparse screenshots {} "$targetFolder/$language" \;
	done

	xcrun simctl shutdown $deviceUUID
	echo "$simulator shutdown"
done

Note: This script requires xcparse and Xcode command line tools to be installed.

Step 3: Cleaning Up File Names

The screenshots include simulator UUIDs in their file names, which need to be removed for clarity. I used an AppleScript to rename them.

on run {input, parameters}
	repeat with myFile in input
		tell application "System Events"
			set myName to the characters 1 thru ((offset of ".png" in (name of myFile as text)) - 1) of (name of myFile as text)
			tell application "Finder"
				set myExtention to name extension of (myFile as alias)
				set myNewName to characters 1 thru (((length of myName) - 37) as number) of (myName as text)
				set name of file (myFile as text) to (myNewName & "." & myExtention as text)
			end tell
		end tell
	end repeat
end run

This produces clean file names for the next step.

Step 4: Framing Screenshots

To make screenshots look professional, I use ScreenshotFramer, an app by the MindNode team. It offers a user-friendly interface to add localised text, background colours, and other design elements. To frame screenshots for all devices, run this Terminal command:

Screenshot-Framer-CLI -project .

Step 5: Uploading to App Store Connect

I use fastlane deliver to upload the framed screenshots to App Store Connect. Their website provides excellent setup instructions. I’m also working on an Xcode Project Plugin using the App Store Connect API to handle screenshots and metadata, but it’s on hold while I explore Vision Pro development. I’ll resume it later.

The Results

Manually creating screenshots took me a full day and caused wrist strain. Now, the automated process takes just 30 minutes on an M1 Mac Mini:

I run this workflow with every Quiet release to keep screenshots up to date. The initial setup took about half a day, but the time savings make it worthwhile.


Category:

Year: