· Workflow · 9 min read

Automating SwiftUI App Store Screenshots Without Fastlane (2026)

Automating SwiftUI App Store Screenshots Without Fastlane (2026)
TL;DR. Fastlane is not the only way to automate iOS store screenshots — it is just the most famous. Apple ships everything you need natively: XCUITest + XCUIScreenshot to capture the live app, the new Swift Testing attachments API to do the same with less boilerplate, and ImageRenderer to render a SwiftUI view straight to a PNG with no simulator run. OSS like swift-snapshot-testing and AppScreenshotKit wrap those. None of them compose the marketing carousel — frames, captions, backgrounds, 50-locale copy. That is a separate step, and it is where Mokbi fits: it composes and localizes the screenshots you captured. It does not capture them.

Fastlane's snapshot is the default answer for "automate my App Store screenshots," and for good reason — it is mature and CI-friendly. But it is also a Ruby toolchain, a Snapfile, a SnapshotHelper.swift you copy into your test target, and a non-trivial amount of setup for what is, underneath, a thin wrapper around the same XCUITest APIs you already have. If you build in SwiftUI and you would rather not add a Ruby dependency to your iOS project, you can automate screenshots with nothing but Xcode and Swift. This post is the honest map of the native options in 2026, with code you can paste, plus a clear line about which tool does which job. For the head-to-head trade-off, see Mokbi vs Fastlane.

One framing first, because it saves a lot of confusion: capturing a frame (driving the app to a state and saving a PNG of what is on screen) and composing a store screenshot (that frame inside a device mockup, with a headline, a background, and translated copy, exported at every required dimension) are two different problems. Everything in this post solves the first. None of it solves the second.

Why skip Fastlane at all?

Nothing is wrong with Fastlane. The reasons developers reach for the native path are practical, not ideological:

  • No Ruby in the loop. Fastlane is a Ruby gem with its own version churn (Bundler, gem conflicts, the occasional CI break after a Ruby bump). If your project is otherwise pure Swift, that is a whole runtime you maintain for one job.
  • You already have the test target. snapshot generates UI tests under the hood. If you are writing UI tests anyway, capturing a screenshot is two extra lines — no new tool required.
  • SwiftUI changed the math. With ImageRenderer you can render a screen to a PNG without launching the app at all, which Fastlane can't do — it always drives the simulator.
  • Fewer moving parts in CI. A plain xcodebuild test that emits screenshots is easier to reason about than a Fastlane lane that shells out to xcodebuild and then post-processes a results bundle.

The flip side: Fastlane bundles the annoying parts (collecting attachments out of the .xcresult bundle, organizing them per locale × device, generating an HTML preview, and uploading via deliver) into one command. Go native and some of that collection plumbing becomes your problem. That trade is the whole decision, and it is worth making consciously.

Option 1: XCUITest + XCUIScreenshot (capture the live app)

This is the foundation Fastlane itself is built on. A UI test target launches your real app in the simulator, drives it with the same finder/tap API as any UI test, and XCUIScreen.main captures the entire device screen at native resolution. You wrap that screenshot in an XCTAttachment and set lifetime = .keepAlways so Xcode keeps it even when the test passes (the default discards attachments on success).

// ScreenshotUITests.swift  — runs in a UI test target
import XCTest

final class ScreenshotUITests: XCTestCase {
    func testCaptureCarouselScreens() {
        let app = XCUIApplication()
        // A launch argument your app reads to seed demo data + hide
        // anything that would make a noisy screenshot (debug banners, etc).
        app.launchArguments += ["-FASTLANE_SNAPSHOT", "NO", "-SCREENSHOT_MODE", "YES"]
        app.launch()

        // Drive the app to the state you want, then capture the WHOLE screen.
        snapshot("01_Home")

        app.buttons["openLibrary"].tap()
        snapshot("02_Library")
    }

    private func snapshot(_ name: String) {
        // XCUIScreen.main captures the full device screen at native
        // resolution — exactly what App Store Connect wants.
        let shot = XCUIScreen.main.screenshot()
        let attachment = XCTAttachment(screenshot: shot)
        attachment.name = name
        attachment.lifetime = .keepAlways // or Xcode discards it when the test passes
        add(attachment)
    }
}

The screenshots land inside the .xcresult bundle. You extract them after the run — either by opening the result bundle in Xcode and dragging them out, or in CI with a tool like xcparse / xc-screenshot that walks the bundle and writes named PNGs to a folder. This extraction step is exactly the part Fastlane automates for you, and it is the main thing you take on by going native.

Localization without code changes. The neat trick here is the Xcode test plan: create one test plan, add a configuration per language/region (each sets the app language + region), and the same test runs once per configuration. You get localized screenshots — the app UI rendered in each locale — without touching the test code. Run on a 6.9-inch iPhone class simulator and a 13-inch iPad class simulator to hit the resolutions App Store Connect requires.

Pros and cons:

  • Real app pixels. Actual running views — fonts, theme, dynamic data, all native — at the simulator's exact resolution.
  • No Fastlane, no Ruby. Pure Xcode + Swift. Runs under xcodebuild test in any CI.
  • Locale is a test-plan config, not code. Add a configuration per language and re-run; no per-locale branches in Swift.
  • You own the extraction. Pulling PNGs out of the .xcresult bundle is on you (a one-time script, but real).
  • Maintenance cost. The finders (buttons["openLibrary"], taps) break when the UI changes, like any UI test.

Option 2: Swift Testing attachments (the modern, lighter version)

Swift Testing (Apple's macro-based framework that ships with Xcode 26) added an Attachment API that does what XCTAttachment did, with less ceremony and a nicer call site. You still drive the app with XCUIApplication and capture with XCUIScreen.main.screenshot() — those live in XCUITest — but you record the image with Attachment.record(...) from a @Test function. The attachment API doesn't care how the image was produced; it accepts any conforming image type and writes it into the test's result bundle.

// ScreenshotTests.swift  — Swift Testing (Xcode 26+)
import Testing
import XCTest // still needed for XCUIApplication / XCUIScreen

@MainActor
struct ScreenshotTests {
    @Test func homeAndLibrary() async throws {
        let app = XCUIApplication()
        app.launchArguments += ["-SCREENSHOT_MODE", "YES"]
        app.launch()

        // Attachment.record attaches to THIS test's result bundle.
        let home = XCUIScreen.main.screenshot()
        Attachment.record(home.image, named: "01_Home.png")

        app.buttons["openLibrary"].tap()
        let library = XCUIScreen.main.screenshot()
        Attachment.record(library.image, named: "02_Library.png")
    }
}

Functionally this captures the same live-app pixels as Option 1 — it is the same underlying screen capture. What you gain is the cleaner Swift Testing ergonomics (parameterized tests, async by default, no XCTestCase subclassing) if your project has already moved to Swift Testing. What you don't escape is the same extraction step: the PNG still lives in the result bundle and still has to be pulled out for upload. Use this when you're already on Swift Testing; otherwise Option 1 is identical in outcome.

Option 3: ImageRenderer (no simulator run at all)

This is the option Fastlane structurally cannot offer. ImageRenderer (SwiftUI, iOS 16+ / macOS 13+) takes a SwiftUI view and renders its hierarchy directly to a UIImage / CGImage — no app launch, no simulator UI automation, no finders to break. You give it a view sized to the exact App Store pixel dimensions and read back PNG bytes.

// Render a SwiftUI view straight to a PNG — no simulator UI run.
import SwiftUI

@MainActor
func renderScreen() -> Data? {
    let view = HomeScreen()                 // your real SwiftUI screen
        .frame(width: 1320, height: 2868)   // 6.9-inch iPhone, exact pixels at 1x

    let renderer = ImageRenderer(content: view)
    renderer.scale = 1                       // size is already in target pixels
    renderer.isOpaque = true                 // App Store rejects alpha channels

#if canImport(UIKit)
    return renderer.uiImage?.pngData()
#else
    guard let cg = renderer.cgImage else { return nil }
    let rep = NSBitmapImageRep(cgImage: cg)
    return rep.representation(using: .png, properties: [:])
#endif
}

Two things matter for store output. Set isOpaque = true — App Store Connect rejects screenshots with an alpha channel, and an opaque render avoids that. And mind the scale: if you size the view in points rather than target pixels, set renderer.scale to your device scale so the output isn't fuzzy; if you size the frame in exact target pixels (as above), keep scale at 1.

The big caveat is what ImageRenderer can't render. It renders SwiftUI, not UIKit/AppKit-backed views: web views, map views, media players, camera previews, and some system controls come out as placeholders. For a clean, deterministic SwiftUI screen it is fantastic — instant, headless, perfectly sized, runs anywhere including on-device. For a screen that embeds platform views, you're back to Options 1–2 to capture the real compositor output.

  • Fastest by far. Milliseconds per image, no simulator boot, no UI driving. Great in CI.
  • Exact dimensions, trivially. Size the frame to 1320 × 2868 (or any required size) and that's your output — no "resize to App Store" guesswork.
  • You construct the state. Because there's no running app, you pass mock data into the view directly — deterministic, no demo-seeding launch arguments.
  • SwiftUI only. Platform-backed views render as placeholders. Verify your screen before trusting it.

Option 4: OSS that wraps the above

Two open-source projects come up constantly, solving adjacent problems.

pointfree/swift-snapshot-testing is the most popular SwiftUI snapshot library, and it is worth being precise about its purpose: it is built for regression testing, not for producing store assets. The first run records a reference PNG and fails on purpose; later runs diff against that reference to catch the day a padding or font change shifts your UI.

// pointfree/swift-snapshot-testing — built for regression, not store assets.
import SnapshotTesting
import SwiftUI
import Testing

@MainActor
struct HomeSnapshotTests {
    @Test func home_iPhone16ProMax() {
        let view = HomeScreen()
        // First run records the reference PNG and FAILS on purpose;
        // later runs diff against it. To re-record: record: .all
        assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13ProMax)))
    }
}

You can repurpose the recorded reference images as raw screenshots, and the .image(layout: .device(...)) strategies render at real device sizes — but that fights the tool's strictness (it wants pixel-perfect matches; antialiasing differences across machines cause flaky failures). Reach for it to guard your UI against regressions, not as your primary screenshot pipeline.

AppScreenshotKit (by shitamori1272) is purpose-built for the job. You declare a SwiftUI view with an @AppScreenshot macro specifying device classes and locales, wrap your real screen in DeviceView so it renders inside an Apple device frame, and export from a Swift Testing function. It organizes output into a clean Screenshots/<locale>/<device>/ tree.

// AppScreenshotKit — declarative SwiftUI screenshots, runs under Swift Testing.
import AppScreenshotKit
import SwiftUI

@AppScreenshot(.iPhone69Inch(), options: .locale([Locale(identifier: "en_US")]))
struct HomeShot: View {
    var body: some View {
        DeviceView { HomeScreen() } // your real screen, wrapped in a device frame
    }
}

// In a test target:
import AppScreenshotKitTestTools
import Testing

@Test @MainActor
func exportScreens() throws {
    let out = URL(fileURLWithPath: "Screenshots")
    let exporter = AppScreenshotExporter(option: .file(outputURL: out))
    try exporter.export(HomeShot.self) // → Screenshots/en_US/iPhone_6_9_inch/...
}

Related tooling in this space is worth knowing about: CLIs like storescreens drive the whole App Store Connect pipeline (XCUITest capture across simulators in parallel, framed renders with captions, metadata upload via Apple's API) from one config file. These are genuinely useful if you want a single, idempotent, CI-friendly chain. They also bring their own opinions about caption layout — which is the seam where a dedicated visual editor tends to win for the marketing surface.

The part none of the above does: composition + 50-language localization

Here is the bounded, honest paragraph. Every option above produces bare frames — your app UI at a device resolution (or, with AppScreenshotKit and friends, a frame inside a plain device mockup). App Store carousels that convert are not bare frames: they are the frame inside a styled device mockup, on a background, under a short headline, often as a multi-panel sequence, repeated across every locale you target. That composition step is what Mokbi is for. You bring the frames you captured (from XCUITest, ImageRenderer, the simulator, wherever), drop them into a browser editor, add device frames and captions and backgrounds, translate the captions across up to 50 App Store languages in roughly one click, and batch-export at every required size. It is free to design; export and publishing come with a subscription (Solo €29.99/mo or Studio €49.99/mo). To be explicit about the boundary: Mokbi does not capture source screenshots — it does not run your app or render your views. Capture stays with the native Swift tooling above; Mokbi composes and localizes what that tooling produces.

A realistic combined SwiftUI workflow

  1. Capture the source frames natively. For deterministic SwiftUI screens, render them with ImageRenderer at exact pixel sizes (Option 3) — it's the fastest and needs no simulator. For screens with platform views or where you want the true live state, write an XCUITest (Option 1) or Swift Testing (Option 2) capture and run on a 6.9-inch iPhone class and 13-inch iPad class simulator.
  2. Localize the capture if you need it. Add a test-plan configuration per language so the app UI renders in each locale, or pass a locale into the view you hand to ImageRenderer. This localizes the app pixels — not your marketing headline.
  3. Compose the marketing carousel. Drop the frames into Mokbi, add styled frames/captions/backgrounds, build the multi-panel sequence. This is the step the native tooling can't do.
  4. Translate captions and export. One-click translate the headline copy across your target App Store locales, then batch-export every required dimension.
  5. On the next UI change, re-capture and re-open. Re-run the renderer or the UI test, re-open the saved Mokbi project, swap the frames, re-export. The composition and translations are preserved.

When to skip automation entirely

If you ship one to three releases a year, building any screenshot harness — native or Fastlane — will cost you more hours than it ever saves. Run the app in the iOS Simulator at the right device class, navigate to each screen, hit the simulator's screenshot command (it captures at exact device resolution), and you have App Store-ready PNGs in ten minutes. Then compose and localize once and move on. The automation above pays off precisely when "re-shoot by hand" stops being a ten-minute job — frequent releases, many locales, or several apps in a portfolio. Match the machinery to your release cadence, not to what looks rigorous. The same logic applies on the other platforms; see the React Native version of this problem and the broader Fastlane vs no-code trade-off.

Whatever you capture with, sanity-check the targets before exporting: get the exact pixel dimensions and format rules (PNG/JPEG, RGB, no alpha channel) from the App Store screenshot sizes guide. Apple rejects off-by-one dimensions with no tolerance, so this is worth thirty seconds.

What to read next

Open the editor →