· Workflow · 9 min czytania

Automatyzacja zrzutów ekranu App Store dla SwiftUI bez fastlane (2026)

Automatyzacja zrzutów ekranu App Store dla SwiftUI bez fastlane (2026)
TL;DR. Fastlane to nie jedyny sposób na automatyzację zrzutów ekranu ze sklepu iOS — po prostu ten najbardziej znany. Apple dostarcza natywnie wszystko, czego potrzebujesz: XCUITest + XCUIScreenshot do przechwytywania działającej aplikacji, nowsze API załączników Swift Testing do tego samego z mniejszą ilością boilerplate'u oraz ImageRenderer do renderowania widoku SwiftUI wprost do PNG bez uruchamiania symulatora. OSS takie jak swift-snapshot-testing i AppScreenshotKit owijają te narzędzia. Żadne z nich nie komponuje karuzeli marketingowej — ramek, podpisów, teł, tekstów w 50 lokalizacjach. To osobny etap, i to właśnie w nim mieści się Mokbi: komponuje i lokalizuje przechwycone przez ciebie zrzuty ekranu. Nie przechwytuje ich.

Moduł snapshot fastlane to domyślna odpowiedź na pytanie "jak zautomatyzować zrzuty ekranu App Store", i nie bez powodu — jest dojrzały i przyjazny dla CI. Ale to też cały toolchain w Ruby, Snapfile, plik SnapshotHelper.swift, który kopiujesz do swojego celu testowego, i nietrywialna ilość konfiguracji dla czegoś, co pod spodem jest cienką nakładką na te same API XCUITest, które już masz. Jeśli budujesz w SwiftUI i wolałbyś nie dodawać zależności od Ruby do swojego projektu iOS, możesz zautomatyzować zrzuty ekranu wyłącznie za pomocą Xcode i Swift. Ten wpis to uczciwa mapa natywnych opcji w 2026 roku, z kodem, który możesz wkleić, i jasnym podziałem, które narzędzie robi co. Bezpośrednie porównanie znajdziesz w artykule Mokbi vs Fastlane.

Na początek jedno rozgraniczenie, bo oszczędza sporo nieporozumień: przechwycenie klatki (doprowadzenie aplikacji do określonego stanu i zapisanie PNG tego, co jest na ekranie) i skomponowanie zrzutu ekranu sklepowego (ta klatka wewnątrz makiety urządzenia, z nagłówkiem, tłem i przetłumaczonym tekstem, wyeksportowana w każdym wymaganym wymiarze) to dwa różne problemy. Wszystko w tym wpisie rozwiązuje pierwszy. Nic z tego nie rozwiązuje drugiego.

Dlaczego w ogóle pomijać fastlane?

Z fastlane nie ma nic złego. Powody, dla których deweloperzy sięgają po ścieżkę natywną, są praktyczne, nie ideologiczne:

  • Bez Ruby w pętli. Fastlane to gem Ruby z własną rotacją wersji (Bundler, konflikty gemów, okazjonalna awaria CI po podbiciu Ruby). Jeśli twój projekt jest poza tym czystym Swiftem, to cały dodatkowy runtime, który utrzymujesz dla jednego zadania.
  • Cel testowy już masz. snapshot pod spodem generuje testy UI. Jeśli i tak piszesz testy UI, przechwycenie zrzutu ekranu to dwie dodatkowe linijki — bez żadnego nowego narzędzia.
  • SwiftUI zmieniło rachunek. Dzięki ImageRenderer możesz wyrenderować ekran do PNG bez w ogóle uruchamiania aplikacji, czego fastlane zrobić nie potrafi — zawsze steruje symulatorem.
  • Mniej ruchomych części w CI. Zwykły xcodebuild test, który generuje zrzuty ekranu, łatwiej zrozumieć niż lane fastlane, który wywołuje xcodebuild, a potem przetwarza paczkę wyników.

Druga strona medalu: fastlane pakuje uciążliwe elementy (zbieranie załączników z paczki .xcresult, organizowanie ich per lokalizacja × urządzenie, generowanie podglądu HTML i przesyłanie przez deliver) w jedno polecenie. Idąc natywnie, część tej hydrauliki zbierania staje się twoim problemem. To cała ta wymiana i warto ją podjąć świadomie.

Opcja 1: XCUITest + XCUIScreenshot (przechwycenie działającej aplikacji)

To fundament, na którym zbudowane jest samo fastlane. Cel testów UI uruchamia twoją prawdziwą aplikację w symulatorze, steruje nią tym samym API finder/tap co dowolny test UI, a XCUIScreen.main przechwytuje cały ekran urządzenia w rozdzielczości natywnej. Ten zrzut ekranu owijasz w XCTAttachment i ustawiasz lifetime = .keepAlways, żeby Xcode zachował go nawet wtedy, gdy test przechodzi (domyślnie załączniki są odrzucane przy sukcesie).

// 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)
    }
}

Zrzuty ekranu lądują wewnątrz paczki .xcresult. Wyciągasz je po uruchomieniu — albo otwierając paczkę wyników w Xcode i przeciągając je na zewnątrz, albo w CI narzędziem takim jak xcparse / xc-screenshot, które przechodzi przez paczkę i zapisuje nazwane PNG-i do folderu. Ten etap ekstrakcji to dokładnie to, co za ciebie automatyzuje fastlane, i to główna rzecz, którą bierzesz na siebie, idąc natywnie.

Lokalizacja bez zmian w kodzie. Fajną sztuczką jest tu plan testów Xcode: tworzysz jeden plan testów, dodajesz konfigurację per język/region (każda ustawia język aplikacji + region), i ten sam test uruchamia się raz na konfigurację. Dostajesz zlokalizowane zrzuty ekranu — UI aplikacji wyrenderowane w każdej lokalizacji — bez dotykania kodu testów. Uruchom na symulatorze klasy iPhone 6,9 cala i symulatorze klasy iPad 13 cali, żeby trafić w rozdzielczości wymagane przez App Store Connect.

Zalety i wady:

  • Prawdziwe piksele aplikacji. Rzeczywiste działające widoki — fonty, motyw, dynamiczne dane, wszystko natywne — w dokładnej rozdzielczości symulatora.
  • Bez fastlane, bez Ruby. Czyste Xcode + Swift. Działa pod xcodebuild test w dowolnym CI.
  • Lokalizacja to konfiguracja planu testów, nie kod. Dodaj konfigurację per język i uruchom ponownie; bez rozgałęzień per lokalizacja w Swift.
  • Ekstrakcja jest po twojej stronie. Wyciąganie PNG-ów z paczki .xcresult to twoje zadanie (jednorazowy skrypt, ale realny).
  • Koszt utrzymania. Findery (buttons["openLibrary"], tapnięcia) psują się przy zmianach UI, jak w każdym teście UI.

Opcja 2: załączniki Swift Testing (nowocześniejsza, lżejsza wersja)

Swift Testing (framework Apple oparty na makrach, dostarczany z Xcode 26) dodał API Attachment, które robi to, co robił XCTAttachment, ale z mniejszą ceremonią i przyjemniejszym miejscem wywołania. Aplikacją nadal sterujesz przez XCUIApplication i przechwytujesz przez XCUIScreen.main.screenshot() — to żyje w XCUITest — ale obraz zapisujesz przez Attachment.record(...) z funkcji @Test. API załączników nie obchodzi, jak obraz powstał; przyjmuje każdy zgodny typ obrazu i zapisuje go do paczki wyników testu.

// 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")
    }
}

Funkcjonalnie przechwytuje to te same piksele działającej aplikacji co Opcja 1 — to to samo bazowe przechwytywanie ekranu. To, co zyskujesz, to czystsza ergonomia Swift Testing (testy parametryzowane, domyślnie async, bez podklasowania XCTestCase), jeśli twój projekt już przeszedł na Swift Testing. To, czego nie unikniesz, to ten sam etap ekstrakcji: PNG nadal żyje w paczce wyników i nadal trzeba go wyciągnąć do przesłania. Sięgnij po to, jeśli już jesteś na Swift Testing; w przeciwnym razie Opcja 1 daje identyczny efekt.

Opcja 3: ImageRenderer (bez żadnego uruchamiania symulatora)

To opcja, której fastlane strukturalnie nie może zaoferować. ImageRenderer (SwiftUI, iOS 16+ / macOS 13+) bierze widok SwiftUI i renderuje jego hierarchię bezpośrednio do UIImage / CGImage — bez uruchamiania aplikacji, bez automatyzacji UI symulatora, bez finderów, które mogłyby się zepsuć. Podajesz mu widok o dokładnych wymiarach pikseli App Store i odczytujesz z powrotem bajty PNG.

// 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
}

Dwie rzeczy mają znaczenie dla wyjścia sklepowego. Ustaw isOpaque = true — App Store Connect odrzuca zrzuty ekranu z kanałem alfa, a nieprzezroczysty render tego unika. I zwróć uwagę na scale: jeśli wymiarujesz widok w punktach, a nie w docelowych pikselach, ustaw renderer.scale na skalę swojego urządzenia, żeby wynik nie był rozmyty; jeśli wymiarujesz ramkę w dokładnych docelowych pikselach (jak powyżej), zostaw skalę na 1.

Duże zastrzeżenie dotyczy tego, czego ImageRenderer nie potrafi wyrenderować. Renderuje SwiftUI, nie widoki oparte na UIKit/AppKit: widoki web, widoki map, odtwarzacze mediów, podglądy kamery i niektóre kontrolki systemowe wychodzą jako placeholdery. Dla czystego, deterministycznego ekranu SwiftUI jest fantastyczny — natychmiastowy, bezgłowy, idealnie wymiarowany, działa wszędzie, w tym na urządzeniu. Dla ekranu, który osadza widoki platformowe, wracasz do Opcji 1–2, żeby przechwycić rzeczywisty wynik kompozytora.

  • Zdecydowanie najszybszy. Milisekundy na obraz, bez rozruchu symulatora, bez sterowania UI. Świetny w CI.
  • Dokładne wymiary, banalnie. Wymiaruj ramkę na 1320 × 2868 (lub dowolny wymagany rozmiar) i to jest twój wynik — bez zgadywania "przeskaluj do App Store".
  • Sam konstruujesz stan. Ponieważ nie ma działającej aplikacji, przekazujesz dane mockowe bezpośrednio do widoku — deterministycznie, bez argumentów uruchomieniowych do zasiewania demo.
  • Tylko SwiftUI. Widoki oparte na platformie renderują się jako placeholdery. Zweryfikuj swój ekran, zanim mu zaufasz.

Opcja 4: OSS, które owija powyższe

Stale pojawiają się dwa projekty open source, rozwiązujące sąsiadujące problemy.

pointfree/swift-snapshot-testing to najpopularniejsza biblioteka do snapshotów SwiftUI, i warto precyzyjnie określić jej cel: jest zbudowana do testów regresji, nie do produkowania materiałów sklepowych. Pierwsze uruchomienie zapisuje referencyjny PNG i celowo zawodzi; kolejne uruchomienia porównują różnice z tą referencją, żeby wychwycić dzień, w którym zmiana paddingu lub fontu przesuwa twoje 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)))
    }
}

Możesz przekierować zapisane obrazy referencyjne na surowe zrzuty ekranu, a strategie .image(layout: .device(...)) renderują w rzeczywistych rozmiarach urządzeń — ale to walczy z rygorystycznością narzędzia (chce dopasowań pixel-perfect; różnice antyaliasingu między maszynami powodują niestabilne awarie). Sięgaj po nie, żeby chronić swoje UI przed regresjami, nie jako główny pipeline zrzutów ekranu.

AppScreenshotKit (autorstwa shitamori1272) jest zbudowany celowo do tego zadania. Deklarujesz widok SwiftUI z makrem @AppScreenshot określającym klasy urządzeń i lokalizacje, owijasz swój prawdziwy ekran w DeviceView, żeby renderował się wewnątrz ramki urządzenia Apple, i eksportujesz z funkcji Swift Testing. Organizuje wynik w czyste drzewo Screenshots/<locale>/<device>/.

// 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/...
}

Warto znać też powiązane narzędzia z tej przestrzeni: CLI takie jak storescreens obsługują cały pipeline App Store Connect (przechwytywanie XCUITest równolegle na wielu symulatorach, oprawione rendery z podpisami, przesyłanie metadanych przez API Apple) z jednego pliku konfiguracyjnego. Są naprawdę użyteczne, jeśli chcesz jeden, idempotentny, przyjazny dla CI łańcuch. Wnoszą też własne założenia co do układu podpisów — a to jest szew, w którym dedykowany edytor wizualny zwykle wygrywa na powierzchni marketingowej.

Czego nie robi żadna z powyższych opcji: kompozycja i lokalizacja na 50 języków

Oto ograniczony, uczciwy akapit. Każda opcja powyżej produkuje nagie klatki — UI twojej aplikacji w rozdzielczości urządzenia (lub, w przypadku AppScreenshotKit i podobnych, klatkę wewnątrz zwykłej makiety urządzenia). Karuzele App Store, które konwertują, to nie nagie klatki: to klatka wewnątrz stylizowanej makiety urządzenia, na tle, pod krótkim nagłówkiem, często jako sekwencja wielopanelowa, powtórzona w każdej lokalizacji, którą obsługujesz. Ten etap kompozycji to właśnie to, do czego służy Mokbi. Przynosisz przechwycone klatki (z XCUITest, ImageRenderer, symulatora, skądkolwiek), wrzucasz je do edytora w przeglądarce, dodajesz ramki urządzeń, podpisy i tła, tłumaczysz podpisy na maksymalnie 50 języków App Store w mniej więcej jednym kliknięciu i eksportujesz wsadowo w każdym wymaganym rozmiarze. Projektowanie jest bezpłatne z podglądowym znakiem wodnym; nieograniczony eksport i publikowanie są dostępne w ramach subskrypcji — Solo za €29.99/mo (1 aplikacja) lub Studio za €49.99/mo (do 5 aplikacji), bez jednorazowego zakupu. Żeby jasno określić granicę: Mokbi nie przechwytuje źródłowych zrzutów ekranu — nie uruchamia twojej aplikacji ani nie renderuje twoich widoków. Przechwytywanie zostaje po stronie natywnych narzędzi Swift opisanych powyżej; Mokbi komponuje i lokalizuje to, co te narzędzia produkują.

Realistyczny połączony przepływ pracy dla SwiftUI

  1. Przechwyć źródłowe klatki natywnie. Dla deterministycznych ekranów SwiftUI wyrenderuj je przez ImageRenderer w dokładnych rozmiarach pikseli (Opcja 3) — jest najszybsza i nie potrzebuje symulatora. Dla ekranów z widokami platformowymi lub gdy chcesz prawdziwego stanu działającego, napisz przechwytywanie XCUITest (Opcja 1) lub Swift Testing (Opcja 2) i uruchom na symulatorze klasy iPhone 6,9 cala i klasy iPad 13 cali.
  2. Zlokalizuj przechwytywanie, jeśli tego potrzebujesz. Dodaj konfigurację planu testów per język, żeby UI aplikacji renderowało się w każdej lokalizacji, albo przekaż lokalizację do widoku, który podajesz do ImageRenderer. To lokalizuje piksele aplikacji — nie twój nagłówek marketingowy.
  3. Skomponuj karuzelę marketingową. Wrzuć klatki do Mokbi, dodaj stylizowane ramki, podpisy i tła, zbuduj sekwencję wielopanelową. To etap, którego natywne narzędzia nie potrafią zrobić.
  4. Przetłumacz podpisy i wyeksportuj. Jednym kliknięciem przetłumacz tekst nagłówka na docelowe lokalizacje App Store, a następnie wyeksportuj wsadowo każdy wymagany wymiar.
  5. Przy kolejnej zmianie UI ponownie przechwyć i ponownie otwórz. Uruchom ponownie renderer lub test UI, otwórz ponownie zapisany projekt Mokbi, podmień klatki, wyeksportuj ponownie. Kompozycja i tłumaczenia są zachowane.

Kiedy w ogóle pominąć automatyzację

Jeśli publikujesz od jednego do trzech wydań rocznie, zbudowanie jakiejkolwiek uprzęży do zrzutów ekranu — natywnej czy fastlane — kosztuje cię więcej godzin, niż kiedykolwiek zaoszczędzi. Uruchom aplikację w iOS Simulator w odpowiedniej klasie urządzenia, przejdź do każdego ekranu, wywołaj polecenie zrzutu ekranu symulatora (przechwytuje w dokładnej rozdzielczości urządzenia) i masz gotowe do App Store PNG-i w dziesięć minut. Potem skomponuj i zlokalizuj raz i idź dalej. Powyższa automatyzacja opłaca się dokładnie wtedy, gdy "ponowne przechwycenie ręczne" przestaje być dziesięciominutowym zadaniem — częste wydania, wiele lokalizacji lub kilka aplikacji w portfolio. Dopasuj maszynerię do swojego tempa wydań, nie do tego, co wygląda rygorystycznie. Ta sama logika dotyczy innych platform; zobacz wersję tego problemu dla React Native i szerszy kompromis fastlane vs no-code.

Niezależnie od tego, czym przechwytujesz, sprawdź cele przed eksportem: dokładne wymiary pikseli i zasady formatu (PNG/JPEG, RGB, bez kanału alfa) znajdziesz w przewodniku po rozmiarach zrzutów ekranu App Store. Apple odrzuca wymiary rozjechane choćby o jeden piksel, bez tolerancji, więc to warte trzydziestu sekund.

Co przeczytać dalej

Otwórz edytor →