$npx -y skills add greenstevester/fastlane-skill --skill snapshotAutomate App Store screenshot capture across devices and languages
| 1 | ## Automated App Store Screenshots |
| 2 | |
| 3 | Set up Fastlane Snapshot to automatically capture App Store screenshots across multiple devices and languages. |
| 4 | |
| 5 | ### Pre-flight Checks |
| 6 | - Fastlane installed: !`fastlane --version 2>/dev/null | grep "fastlane " | head -1 || echo "✗ Not installed - run: brew install fastlane"` |
| 7 | - Fastfile exists: !`ls fastlane/Fastfile 2>/dev/null && echo "✓ Found" || echo "✗ Not found - run /setup-fastlane first"` |
| 8 | - Existing Snapfile: !`ls fastlane/Snapfile 2>/dev/null && echo "✓ Already configured" || echo "○ Not configured yet"` |
| 9 | - UI Test target: !`find . -maxdepth 3 -name "*UITests*" -type d 2>/dev/null | head -1 || echo "○ No UI test target found"` |
| 10 | - Simulators available: !`xcrun simctl list devices available | grep -E "iPhone|iPad" | head -3` |
| 11 | |
| 12 | ### Arguments: ${ARGUMENTS:-setup} |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## Why Automate Screenshots? |
| 17 | |
| 18 | App Store requires screenshots for multiple device sizes. Manual capture means: |
| 19 | - 5+ device sizes × 5+ screenshots × N languages = **hours of work** |
| 20 | - Risk of inconsistency between screenshots |
| 21 | - Repeat everything for each app update |
| 22 | |
| 23 | Snapshot automates this: run once, get all screenshots. |
| 24 | |
| 25 | --- |
| 26 | |
| 27 | ## Step 1: Initialize Snapshot |
| 28 | |
| 29 | ```bash |
| 30 | fastlane snapshot init |
| 31 | ``` |
| 32 | |
| 33 | This creates: |
| 34 | - `fastlane/Snapfile` - Configuration file |
| 35 | - `fastlane/SnapshotHelper.swift` - Helper for UI tests |
| 36 | |
| 37 | --- |
| 38 | |
| 39 | ## Step 2: Configure Snapfile |
| 40 | |
| 41 | Edit `fastlane/Snapfile`: |
| 42 | |
| 43 | ```ruby |
| 44 | # Devices to capture (App Store requirements) |
| 45 | devices([ |
| 46 | "iPhone 15 Pro Max", # 6.7" display (required) |
| 47 | "iPhone 15 Pro", # 6.1" display |
| 48 | "iPhone SE (3rd generation)", # 4.7" display (if supporting older phones) |
| 49 | "iPad Pro 13-inch (M4)", # iPad screenshots (if universal app) |
| 50 | ]) |
| 51 | |
| 52 | # Languages to capture |
| 53 | languages([ |
| 54 | "en-US", |
| 55 | # "ja", # Japanese |
| 56 | # "de-DE", # German |
| 57 | # "fr-FR", # French |
| 58 | # "es-ES", # Spanish |
| 59 | ]) |
| 60 | |
| 61 | # UI Test scheme |
| 62 | scheme("YourAppUITests") |
| 63 | |
| 64 | # Output directory |
| 65 | output_directory("./fastlane/screenshots") |
| 66 | |
| 67 | # Clear old screenshots before capture |
| 68 | clear_previous_screenshots(true) |
| 69 | |
| 70 | # Stop on first error (set false to continue despite failures) |
| 71 | stop_after_first_error(true) |
| 72 | |
| 73 | # Dark mode variants (iOS 13+) |
| 74 | # dark_mode(true) |
| 75 | |
| 76 | # Workspace or project (uncomment one) |
| 77 | # workspace("YourApp.xcworkspace") |
| 78 | # project("YourApp.xcodeproj") |
| 79 | ``` |
| 80 | |
| 81 | --- |
| 82 | |
| 83 | ## Step 3: Add SnapshotHelper to UI Tests |
| 84 | |
| 85 | 1. **Add SnapshotHelper.swift** to your UI test target: |
| 86 | - Drag `fastlane/SnapshotHelper.swift` into Xcode |
| 87 | - Ensure it's added to your **UITests** target (not main app) |
| 88 | |
| 89 | 2. **Import and configure** in your UI test file: |
| 90 | |
| 91 | ```swift |
| 92 | import XCTest |
| 93 | |
| 94 | class ScreenshotTests: XCTestCase { |
| 95 | |
| 96 | override func setUpWithError() throws { |
| 97 | continueAfterFailure = false |
| 98 | let app = XCUIApplication() |
| 99 | setupSnapshot(app) // Initialize snapshot |
| 100 | app.launch() |
| 101 | } |
| 102 | |
| 103 | func testTakeScreenshots() throws { |
| 104 | let app = XCUIApplication() |
| 105 | |
| 106 | // Screenshot 1: Home screen |
| 107 | snapshot("01_HomeScreen") |
| 108 | |
| 109 | // Navigate to feature and capture |
| 110 | app.buttons["Feature"].tap() |
| 111 | snapshot("02_FeatureScreen") |
| 112 | |
| 113 | // Screenshot with content |
| 114 | app.textFields["Search"].tap() |
| 115 | app.textFields["Search"].typeText("Example") |
| 116 | snapshot("03_SearchResults") |
| 117 | |
| 118 | // Settings screen |
| 119 | app.buttons["Settings"].tap() |
| 120 | snapshot("04_Settings") |
| 121 | |
| 122 | // Any additional screens... |
| 123 | snapshot("05_DetailView") |
| 124 | } |
| 125 | } |
| 126 | ``` |
| 127 | |
| 128 | --- |
| 129 | |
| 130 | ## Step 4: Run Snapshot |
| 131 | |
| 132 | ```bash |
| 133 | # Capture all screenshots |
| 134 | fastlane snapshot |
| 135 | |
| 136 | # Specific device only |
| 137 | fastlane snapshot --devices "iPhone 15 Pro Max" |
| 138 | |
| 139 | # Specific language only |
| 140 | fastlane snapshot --languages "en-US" |
| 141 | |
| 142 | # Skip launch (use existing simulator state) |
| 143 | fastlane snapshot --skip_open_summary |
| 144 | ``` |
| 145 | |
| 146 | Screenshots are saved to `fastlane/screenshots/{language}/{device}/`. |
| 147 | |
| 148 | --- |
| 149 | |
| 150 | ## Step 5: Upload to App Store Connect |
| 151 | |
| 152 | After capturing, upload with `deliver`: |
| 153 | |
| 154 | ```bash |
| 155 | # Upload screenshots only (no binary, no metadata) |
| 156 | fastlane deliver --skip_binary_upload --skip_metadata --overwrite_screenshots |
| 157 | ``` |
| 158 | |
| 159 | `deliver` reads `fastlane/screenshots/<locale>/`, maps each PNG to a display size by its **pixel dimensions**, and orders them by **filename sort** — so prefix names (`01_`, `02_`, …). |
| 160 | |
| 161 | ### Uploading screenshots you framed yourself |
| 162 | |
| 163 | If you frame with your own pipeline (a design tool, a web framer) instead of `frameit`, `deliver` can still upload them — stage them into the `<locale>/` layout and let dimension-mapping place them: |
| 164 | |
| 165 | ```ruby |
| 166 | lane :upload_framed_screenshots do |
| 167 | framed = File.expand_path("../path/to/your/framed", __dir__) |
| 168 | staging = File.expand_path("./screenshots/en-US", __dir__) # deliver wants <locale>/ |
| 169 | FileUtils.rm_rf(File.dirname(staging)); FileUtils.mkdir_p(staging) |
| 170 | FileUtils.cp(Dir.glob("#{framed}/*.png"), staging) |
| 171 | deliver(ski |