Mengotomasi tangkapan layar App Store SwiftUI tanpa fastlane (2026)
XCUITest + XCUIScreenshot untuk menangkap aplikasi yang berjalan, API attachment Swift Testing yang baru untuk melakukan hal sama dengan boilerplate lebih sedikit, dan ImageRenderer untuk merender tampilan SwiftUI langsung ke PNG tanpa menjalankan simulator. OSS seperti swift-snapshot-testing dan AppScreenshotKit membungkus semua itu. Tidak satu pun dari mereka yang menyusun korsel marketing — bingkai, teks, latar belakang, salinan 50 lokal. Itu adalah langkah terpisah, dan di sinilah Mokbi berperan: ia menyusun dan melokalisasi tangkapan layar yang sudah kamu ambil. Ia tidak mengambilnya.snapshot milik fastlane adalah jawaban default untuk "otomasi tangkapan layar App Store-ku," dan dengan alasan yang bagus — ia matang dan ramah CI. Tapi ia juga toolchain Ruby, sebuah Snapfile, sebuah SnapshotHelper.swift yang kamu salin ke target test-mu, dan setup yang tidak sedikit untuk sesuatu yang, di baliknya, hanyalah wrapper tipis di sekitar API XCUITest yang sama yang sudah kamu punya. Jika kamu membangun di SwiftUI dan lebih memilih untuk tidak menambahkan dependency Ruby ke proyek iOS-mu, kamu bisa mengotomasi tangkapan layar hanya dengan Xcode dan Swift. Artikel ini adalah peta jujur tentang opsi native di 2026, dengan kode yang bisa kamu tempel, plus garis jelas tentang alat mana yang mengerjakan pekerjaan mana. Untuk perbandingan trade-off berdampingan, lihat Mokbi vs Fastlane.
Satu framing di awal, karena ini menghemat banyak kebingungan: mengambil frame (mengarahkan aplikasi ke suatu kondisi dan menyimpan PNG dari apa yang tampil di layar) dan menyusun tangkapan layar toko (frame itu di dalam mockup perangkat, dengan judul, latar belakang, dan salinan terjemahan, diekspor di setiap dimensi yang diperlukan) adalah dua masalah yang berbeda. Semua yang ada di artikel ini menyelesaikan yang pertama. Tidak ada yang menyelesaikan yang kedua.
Kenapa harus melewati fastlane sama sekali?
Tidak ada yang salah dengan fastlane. Alasan developer beralih ke jalur native bersifat praktis, bukan ideologis:
- Tanpa Ruby di alur kerja. fastlane adalah Ruby gem dengan pergantian versinya sendiri (Bundler, konflik gem, sesekali build CI rusak setelah bump Ruby). Jika proyekmu selain itu murni Swift, itu adalah satu runtime penuh yang harus kamu pelihara untuk satu pekerjaan.
- Kamu sudah punya target test.
snapshotmenghasilkan UI test di baliknya. Jika kamu memang sudah menulis UI test, mengambil tangkapan layar hanya dua baris tambahan — tidak perlu alat baru. - SwiftUI mengubah perhitungannya. Dengan
ImageRendererkamu bisa merender layar ke PNG tanpa meluncurkan aplikasi sama sekali, yang tidak bisa dilakukan fastlane — ia selalu menjalankan simulator. - Lebih sedikit bagian bergerak di CI.
xcodebuild testbiasa yang mengeluarkan tangkapan layar lebih mudah dinalar daripada lane fastlane yang menjalankanxcodebuildlalu memproses hasil bundle-nya.
Sisi lainnya: fastlane membungkus bagian-bagian yang merepotkan (mengumpulkan attachment dari bundle .xcresult, mengaturnya per lokal × perangkat, menghasilkan pratinjau HTML, dan mengunggah lewat deliver) ke dalam satu perintah. Beralih ke native berarti sebagian pekerjaan pengumpulan itu jadi tanggung jawabmu. Trade-off itulah keseluruhan keputusannya, dan layak dibuat secara sadar.
Opsi 1: XCUITest + XCUIScreenshot (menangkap aplikasi yang berjalan)
Ini adalah fondasi tempat fastlane sendiri dibangun. Target UI test meluncurkan aplikasimu yang sebenarnya di simulator, mengarahkannya dengan API finder/tap yang sama seperti UI test mana pun, dan XCUIScreen.main menangkap seluruh layar perangkat pada resolusi native. Kamu membungkus tangkapan layar itu dalam XCTAttachment dan mengatur lifetime = .keepAlways agar Xcode tetap menyimpannya meski test-nya lulus (defaultnya membuang attachment saat berhasil).
// 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)
}
}Tangkapan layarnya masuk ke dalam bundle .xcresult. Kamu mengekstraknya setelah run — baik dengan membuka result bundle di Xcode dan menyeretnya keluar, atau di CI dengan alat seperti xcparse / xc-screenshot yang menelusuri bundle dan menulis PNG bernama ke sebuah folder. Langkah ekstraksi ini persis bagian yang diotomasi fastlane untukmu, dan ini adalah hal utama yang harus kamu tanggung sendiri saat beralih ke native.
Lokalisasi tanpa mengubah kode. Trik rapinya di sini adalah test plan Xcode: buat satu test plan, tambahkan satu konfigurasi per bahasa/wilayah (masing-masing mengatur bahasa + wilayah aplikasi), dan test yang sama berjalan sekali per konfigurasi. Kamu mendapatkan tangkapan layar yang dilokalisasi — UI aplikasi dirender di setiap lokal — tanpa menyentuh kode test. Jalankan di simulator kelas iPhone 6.9-inch dan simulator kelas iPad 13-inch untuk mencapai resolusi yang diminta App Store Connect.
Kelebihan dan kekurangan:
- Piksel aplikasi nyata. Tampilan yang benar-benar berjalan — font, tema, data dinamis, semua native — pada resolusi persis simulator.
- Tanpa fastlane, tanpa Ruby. Murni Xcode + Swift. Berjalan di bawah
xcodebuild testdi CI mana pun. - Lokal adalah konfigurasi test plan, bukan kode. Tambahkan satu konfigurasi per bahasa dan jalankan ulang; tidak ada percabangan per lokal di Swift.
- Kamu yang menanggung ekstraksi. Menarik PNG keluar dari bundle
.xcresultadalah tanggung jawabmu (skrip sekali-buat, tapi nyata). - Biaya pemeliharaan. Finder (
buttons["openLibrary"], tap) rusak saat UI berubah, seperti UI test mana pun.
Opsi 2: attachment Swift Testing (versi modern yang lebih ringan)
Swift Testing (framework berbasis macro milik Apple yang hadir bersama Xcode 26) menambahkan API Attachment yang melakukan hal sama seperti XCTAttachment, dengan lebih sedikit seremoni dan titik pemanggilan yang lebih rapi. Kamu tetap mengarahkan aplikasi dengan XCUIApplication dan menangkap dengan XCUIScreen.main.screenshot() — keduanya ada di XCUITest — tapi kamu merekam gambarnya dengan Attachment.record(...) dari fungsi @Test. API attachment ini tidak peduli bagaimana gambarnya dihasilkan; ia menerima jenis gambar yang sesuai apa pun dan menulisnya ke result bundle test.
// 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")
}
}Secara fungsional ini menangkap piksel aplikasi berjalan yang sama seperti Opsi 1 — mekanisme pengambilan layar yang mendasarinya sama. Yang kamu dapatkan adalah ergonomi Swift Testing yang lebih bersih (test terparameterisasi, async secara default, tanpa subclass XCTestCase) jika proyekmu sudah beralih ke Swift Testing. Yang tidak kamu hindari adalah langkah ekstraksi yang sama: PNG-nya tetap tersimpan di result bundle dan tetap harus ditarik keluar untuk diunggah. Gunakan ini jika kamu sudah menggunakan Swift Testing; kalau tidak, Opsi 1 hasilnya identik.
Opsi 3: ImageRenderer (tanpa menjalankan simulator sama sekali)
Ini adalah opsi yang secara struktural tidak bisa ditawarkan fastlane. ImageRenderer (SwiftUI, iOS 16+ / macOS 13+) mengambil tampilan SwiftUI dan merender hierarkinya langsung ke UIImage / CGImage — tanpa meluncurkan aplikasi, tanpa automasi UI simulator, tanpa finder yang bisa rusak. Kamu memberinya tampilan berukuran sesuai dimensi piksel App Store yang tepat lalu membaca kembali byte PNG-nya.
// 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
}Ada dua hal yang penting untuk output toko. Atur isOpaque = true — App Store Connect menolak tangkapan layar dengan alpha channel, dan render opaque menghindari itu. Lalu perhatikan scale-nya: jika kamu mengukur tampilan dalam points alih-alih piksel target, atur renderer.scale sesuai skala perangkatmu supaya outputnya tidak buram; jika kamu mengukur frame dalam piksel target yang tepat (seperti di atas), pertahankan scale di 1.
Peringatan besarnya adalah apa yang tidak bisa dirender ImageRenderer. Ia merender SwiftUI, bukan tampilan berbasis UIKit/AppKit: web view, map view, pemutar media, pratinjau kamera, dan beberapa kontrol sistem akan tampil sebagai placeholder. Untuk layar SwiftUI yang bersih dan deterministik, ini luar biasa — instan, headless, ukurannya pas sempurna, berjalan di mana saja termasuk di perangkat langsung. Untuk layar yang menyematkan tampilan platform, kamu kembali ke Opsi 1–2 untuk menangkap output compositor yang sebenarnya.
- Jauh lebih cepat. Milidetik per gambar, tanpa boot simulator, tanpa mengarahkan UI. Bagus untuk CI.
- Dimensi tepat, dengan mudah. Ukur frame ke 1320 × 2868 (atau ukuran wajib apa pun) dan itulah outputmu — tanpa tebak-tebakan "ubah ukuran ke App Store".
- Kamu yang membangun kondisinya. Karena tidak ada aplikasi yang berjalan, kamu meneruskan data mock langsung ke tampilan — deterministik, tanpa launch argument untuk seed demo.
- Hanya SwiftUI. Tampilan berbasis platform dirender sebagai placeholder. Verifikasi layarmu sebelum memercayainya.
Opsi 4: OSS yang membungkus semua di atas
Dua proyek open-source terus-menerus muncul, menyelesaikan masalah yang berdekatan.
pointfree/swift-snapshot-testing adalah library snapshot SwiftUI paling populer, dan penting untuk jelas soal tujuannya: ia dibangun untuk pengujian regresi, bukan untuk menghasilkan aset toko. Run pertama merekam PNG referensi dan sengaja gagal; run berikutnya membandingkan dengan referensi itu untuk menangkap hari ketika perubahan padding atau font menggeser UI-mu.
// 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)))
}
}Kamu bisa memanfaatkan gambar referensi yang terekam sebagai tangkapan layar mentah, dan strategi .image(layout: .device(...)) merender pada ukuran perangkat nyata — tapi itu melawan ketegasan alatnya (ia menginginkan kecocokan piksel-sempurna, sedangkan perbedaan antialiasing antar mesin menyebabkan kegagalan yang tidak konsisten). Gunakan ini untuk menjaga UI-mu dari regresi, bukan sebagai pipeline tangkapan layar utamamu.
AppScreenshotKit (oleh shitamori1272) dibangun khusus untuk pekerjaan ini. Kamu mendeklarasikan tampilan SwiftUI dengan macro @AppScreenshot yang menentukan kelas perangkat dan lokal, membungkus layar nyatamu dalam DeviceView agar dirender di dalam bingkai perangkat Apple, dan mengekspor dari fungsi Swift Testing. Ia mengatur output ke dalam struktur Screenshots/<locale>/<device>/ yang rapi.
// 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/...
}Tooling terkait di ranah ini juga layak diketahui: CLI seperti storescreens menjalankan seluruh pipeline App Store Connect (pengambilan XCUITest di banyak simulator secara paralel, render berbingkai dengan teks, unggah metadata lewat API Apple) dari satu file konfigurasi. Ini benar-benar berguna jika kamu menginginkan satu rantai yang idempotent dan ramah CI. Mereka juga membawa pandangan sendiri soal tata letak teks — dan di titik itulah editor visual khusus cenderung unggul untuk permukaan marketing.
Bagian yang tidak dilakukan satu pun di atas: komposisi + lokalisasi 50 bahasa
Inilah paragraf yang jujur dan terbatas. Setiap opsi di atas menghasilkan frame mentah — UI aplikasimu pada resolusi perangkat (atau, dengan AppScreenshotKit dan sejenisnya, frame di dalam mockup perangkat polos). Korsel App Store yang mengkonversi bukan frame mentah: itu adalah frame di dalam mockup perangkat bergaya, di atas latar belakang, di bawah judul singkat, sering sebagai urutan multi-panel, dan diulang di setiap lokal yang kamu targetkan. Langkah komposisi itulah yang menjadi tujuan Mokbi. Kamu membawa frame yang kamu ambil (dari XCUITest, ImageRenderer, simulator, dari mana saja), menjatuhkannya ke editor berbasis browser, menambahkan bingkai perangkat dan teks dan latar belakang, menerjemahkan teks ke hingga 50 bahasa App Store hanya dalam satu klik, dan mengekspor massal di setiap ukuran yang diperlukan. Gratis untuk mendesain dengan pratinjau bertanda air; ekspor tanpa batas dan publikasi hadir bersama langganan — Solo €29.99/mo (1 aplikasi) atau Studio €49.99/mo (hingga 5 aplikasi), tanpa pembelian sekali. Untuk memperjelas batasannya: Mokbi tidak mengambil tangkapan layar sumber — ia tidak menjalankan aplikasimu atau merender tampilanmu. Pengambilan tetap menggunakan tooling Swift native di atas; Mokbi menyusun dan melokalisasi apa yang dihasilkan tooling tersebut.
Alur kerja SwiftUI kombinasi yang realistis
- Ambil frame sumber secara native. Untuk layar SwiftUI yang deterministik, render dengan
ImageRendererpada ukuran piksel tepat (Opsi 3) — ini yang tercepat dan tidak butuh simulator. Untuk layar dengan tampilan platform atau ketika kamu ingin kondisi berjalan yang sebenarnya, tulis tangkapan XCUITest (Opsi 1) atau Swift Testing (Opsi 2) dan jalankan pada simulator kelas iPhone 6.9-inch dan kelas iPad 13-inch. - Lokalisasi tangkapannya jika kamu membutuhkannya. Tambahkan satu konfigurasi test plan per bahasa agar UI aplikasi dirender di setiap lokal, atau teruskan lokal ke tampilan yang kamu berikan ke
ImageRenderer. Ini melokalisasi piksel aplikasi — bukan judul marketing-mu. - Susun korsel marketing. Masukkan frame ke Mokbi, tambahkan bingkai/teks/latar belakang bergaya, bangun urutan multi-panel. Ini adalah langkah yang tidak bisa dilakukan tooling native.
- Terjemahkan teks dan ekspor. Terjemahkan judul dengan satu klik ke lokal App Store targetmu, lalu ekspor massal setiap dimensi yang diperlukan.
- Saat ada perubahan UI berikutnya, ambil ulang dan buka kembali. Jalankan ulang renderer atau UI test-nya, buka kembali proyek Mokbi yang tersimpan, ganti frame, ekspor ulang. Komposisi dan terjemahan tetap tersimpan.
Kapan kamu bisa melewati otomasi sepenuhnya
Jika kamu merilis satu hingga tiga kali setahun, membangun harness tangkapan layar apa pun — native atau fastlane — akan memakan lebih banyak jam daripada yang pernah dihemat. Jalankan aplikasi di iOS Simulator pada kelas perangkat yang tepat, arahkan ke setiap layar, jalankan perintah tangkapan layar simulator (menangkap pada resolusi perangkat persis), dan kamu punya PNG siap App Store dalam sepuluh menit. Lalu susun dan lokalisasi sekali, lanjutkan. Otomasi di atas terbayar tepat ketika "ambil ulang secara manual" berhenti menjadi pekerjaan sepuluh menit — rilis sering, banyak lokal, atau beberapa aplikasi dalam portofolio. Sesuaikan mesinnya dengan ritme rilismu, bukan dengan apa yang terlihat paling ketat. Logika yang sama berlaku di platform lain; lihat versi React Native dari masalah ini dan perbandingan yang lebih luas fastlane vs tanpa kode.
Apa pun cara pengambilanmu, periksa akal sehat targetnya sebelum mengekspor: dapatkan dimensi piksel yang tepat dan aturan format (PNG/JPEG, RGB, tanpa alpha channel) dari panduan ukuran tangkapan layar App Store. Apple menolak dimensi yang meleset satu piksel pun tanpa toleransi, jadi ini layak dilakukan dalam tiga puluh detik.