Fastlane के बिना SwiftUI App Store स्क्रीनशॉट ऑटोमेट करना (2026)
XCUITest + XCUIScreenshot, कम boilerplate में वही काम करने के लिए नया Swift Testing attachments API, और बिना simulator चलाए सीधे SwiftUI view को PNG में रेंडर करने के लिए ImageRenderer। swift-snapshot-testing और AppScreenshotKit जैसे OSS इन्हें wrap करते हैं। इनमें से कोई भी मार्केटिंग कैरोसेल कंपोज़ नहीं करता — फ्रेम, कैप्शन, बैकग्राउंड, 50-locale copy। यह एक अलग चरण है, और यहीं Mokbi फिट होता है: यह आपके कैप्चर किए स्क्रीनशॉट को कंपोज़ और लोकलाइज़ करता है। यह उन्हें कैप्चर नहीं करता।Fastlane का snapshot "मेरे App Store स्क्रीनशॉट ऑटोमेट करो" का डिफ़ॉल्ट जवाब है, और वजह भी सही है — यह परिपक्व और CI-friendly है। लेकिन यह एक Ruby toolchain भी है, एक Snapfile, एक SnapshotHelper.swift जो आप अपने test target में कॉपी करते हैं, और उन्हीं XCUITest APIs के ऊपर एक पतली wrapper के लिए काफ़ी सेटअप है जो आपके पास पहले से हैं। अगर आप SwiftUI में बनाते हैं और अपने iOS प्रोजेक्ट में Ruby dependency नहीं जोड़ना चाहते, तो सिर्फ़ Xcode और Swift से स्क्रीनशॉट ऑटोमेट कर सकते हैं। यह पोस्ट 2026 में नेटिव विकल्पों का ईमानदार नक्शा है, पेस्ट करने योग्य कोड के साथ, और इस बारे में स्पष्ट लकीर कि कौन-सा टूल कौन-सा काम करता है। आमने-सामने trade-off के लिए देखें Mokbi बनाम Fastlane।
एक बात पहले ही स्पष्ट कर लेते हैं, क्योंकि इससे बहुत भ्रम बचता है: फ्रेम कैप्चर करना (ऐप को किसी state में ले जाना और स्क्रीन पर जो है उसका PNG सेव करना) और स्टोर स्क्रीनशॉट कंपोज़ करना (वह फ्रेम एक डिवाइस मॉकअप के अंदर, हेडलाइन के साथ, बैकग्राउंड पर, और translated copy के साथ, हर ज़रूरी डाइमेंशन पर एक्सपोर्ट) — ये दो अलग-अलग समस्याएँ हैं। इस पोस्ट में सब कुछ पहली समस्या हल करता है। इनमें से कोई भी दूसरी नहीं।
Fastlane को छोड़ें ही क्यों?
Fastlane में कुछ भी गलत नहीं है। डेवलपर्स के नेटिव रास्ता चुनने की वजहें व्यावहारिक हैं, वैचारिक नहीं:
- Ruby बीच में नहीं। Fastlane अपने version churn वाला एक Ruby gem है (Bundler, gem conflicts, कभी-कभी Ruby bump के बाद CI टूटना)। अगर आपका प्रोजेक्ट बाकी शुद्ध Swift है, तो यह एक पूरा runtime है जिसे सिर्फ़ एक काम के लिए मेंटेन करना पड़ता है।
- Test target पहले से है।
snapshotपर्दे के पीछे UI tests जेनरेट करता है। अगर वैसे भी UI tests लिख रहे हैं, तो स्क्रीनशॉट कैप्चर करना बस दो अतिरिक्त लाइनें हैं — कोई नया टूल नहीं चाहिए। - SwiftUI ने गणित बदल दिया।
ImageRendererके साथ ऐप लॉन्च किए बिना एक स्क्रीन को PNG में रेंडर कर सकते हैं, जो Fastlane नहीं कर सकता — यह हमेशा simulator ही चलाता है। - CI में कम moving parts। स्क्रीनशॉट emit करने वाला एक सादा
xcodebuild test, Fastlane lane से आसान है जोxcodebuildको shell out करता है और फिर एक results bundle post-process करता है।
दूसरा पहलू: Fastlane परेशान करने वाले हिस्सों को (.xcresult bundle से attachments collect करना, उन्हें प्रति locale × device organize करना, HTML preview जेनरेट करना, और deliver के ज़रिए अपलोड करना) एक ही कमांड में बांध देता है। नेटिव रास्ता चुनने पर यह collection plumbing का कुछ हिस्सा आपकी समस्या बन जाता है। यही पूरा फैसला है, और इसे सोच-समझकर लेना ज़रूरी है।
विकल्प 1: XCUITest + XCUIScreenshot (लाइव ऐप कैप्चर करें)
यह वही नींव है जिस पर Fastlane खुद बना है। एक UI test target simulator में आपका असली ऐप लॉन्च करता है, इसे किसी भी UI test जैसे finder/tap API से चलाता है, और XCUIScreen.main पूरी डिवाइस स्क्रीन को native resolution पर कैप्चर करता है। आप उस स्क्रीनशॉट को एक XCTAttachment में wrap करते हैं और lifetime = .keepAlways सेट करते हैं ताकि Xcode इसे तब भी रखे जब test pass हो जाए (डिफ़ॉल्ट रूप से success पर attachments discard हो जाते हैं)।
// 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)
}
}स्क्रीनशॉट .xcresult bundle के अंदर आकर रुक जाते हैं। रन के बाद इन्हें निकालते हैं — या तो Xcode में result bundle खोलकर उन्हें ड्रैग करके, या CI में xcparse / xc-screenshot जैसे टूल से जो bundle को walk करता है और named PNGs को एक folder में लिखता है। यह extraction चरण ठीक वही हिस्सा है जो Fastlane आपके लिए ऑटोमेट करता है, और नेटिव रास्ता चुनने पर यही सबसे बड़ी ज़िम्मेदारी बनती है।
कोड बदले बिना लोकलाइज़ेशन। यहाँ चालाक तरीका Xcode का test plan है: एक test plan बनाएं, प्रति भाषा/region एक configuration जोड़ें (हर एक ऐप की भाषा + region सेट करती है), और वही test हर configuration के लिए एक बार चलता है। test code को छुए बिना लोकलाइज़्ड स्क्रीनशॉट मिल जाते हैं — हर locale में रेंडर हुआ ऐप UI। App Store Connect को चाहिए resolutions पाने के लिए 6.9-inch iPhone class simulator और 13-inch iPad class simulator पर चलाएं।
फ़ायदे और नुकसान:
- असली ऐप pixels। वास्तव में चल रहे views — fonts, theme, dynamic data, सब नेटिव — simulator के exact resolution पर।
- कोई Fastlane नहीं, कोई Ruby नहीं। शुद्ध Xcode + Swift। किसी भी CI में
xcodebuild testके तहत चलता है। - Locale एक test-plan configuration है, कोड नहीं। प्रति भाषा एक configuration जोड़ें और दोबारा चलाएं; Swift में per-locale branches नहीं चाहिए।
- Extraction आपकी ज़िम्मेदारी है।
.xcresultbundle से PNGs निकालना आप पर है (एक बार का script, पर असली काम)। - Maintenance की लागत। Finders (
buttons["openLibrary"], taps) UI बदलने पर टूट जाते हैं, किसी भी UI test की तरह।
विकल्प 2: Swift Testing attachments (आधुनिक, हल्का वर्ज़न)
Swift Testing (Apple का macro-based framework जो Xcode 26 के साथ आता है) ने एक Attachment API जोड़ा है जो वही करता है जो XCTAttachment करता था, कम ceremony और अच्छे call site के साथ। ऐप अभी भी XCUIApplication से चलाते हैं और XCUIScreen.main.screenshot() से कैप्चर करते हैं — वे XCUITest में रहते हैं — लेकिन image को @Test function से Attachment.record(...) से record करते हैं। Attachment API को इससे फ़र्क नहीं पड़ता कि image कैसे बनी; यह किसी भी conforming image type को स्वीकार करता है और test के 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")
}
}व्यावहारिक रूप से यह विकल्प 1 जैसे ही live-app pixels कैप्चर करता है — यह वही underlying screen capture है। जो मिलता है वह है साफ़ Swift Testing ergonomics (parameterized tests, डिफ़ॉल्ट रूप से async, कोई XCTestCase subclassing नहीं) अगर आपका प्रोजेक्ट पहले से Swift Testing पर shift कर चुका है। जो नहीं बचता वह है वही extraction चरण: PNG अभी भी result bundle में रहता है और अपलोड के लिए फिर भी निकालना पड़ता है। इसे तब उपयोग करें जब पहले से Swift Testing पर हों; वरना outcome में विकल्प 1 से यह बिल्कुल एक-सा है।
विकल्प 3: ImageRenderer (कोई simulator रन ही नहीं)
यह वह विकल्प है जो Fastlane structurally दे ही नहीं सकता। ImageRenderer (SwiftUI, iOS 16+ / macOS 13+) एक SwiftUI view लेता है और उसके hierarchy को सीधे UIImage / CGImage में रेंडर करता है — कोई app launch नहीं, कोई simulator UI automation नहीं, कोई finders नहीं जो टूट सकें। इसे exact App Store pixel dimensions पर sized एक view देते हैं और 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
}स्टोर आउटपुट के लिए दो बातें मायने रखती हैं। isOpaque = true सेट करें — App Store Connect alpha channel वाले स्क्रीनशॉट रिजेक्ट करता है, और opaque रेंडर इससे बचाता है। और scale का ध्यान रखें: अगर view को target pixels की बजाय points में size करते हैं, तो renderer.scale को अपने device scale पर सेट करें ताकि output धुंधला न हो; अगर frame को exact target pixels में size करते हैं (जैसा ऊपर है), तो scale को 1 पर रखें।
सबसे बड़ी चेतावनी यह है कि ImageRenderer क्या नहीं रेंडर कर सकता। यह SwiftUI रेंडर करता है, UIKit/AppKit-backed views नहीं: web views, map views, media players, camera previews, और कुछ system controls placeholders के रूप में आते हैं। एक साफ़, deterministic SwiftUI स्क्रीन के लिए यह शानदार है — instant, headless, बिल्कुल सही size, कहीं भी चलता है यहाँ तक कि on-device भी। जिस स्क्रीन में platform views embed हों, उसके लिए असली compositor output कैप्चर करने के लिए विकल्प 1–2 पर वापस जाना पड़ता है।
- अब तक सबसे तेज़। प्रति image milliseconds, कोई simulator boot नहीं, कोई UI driving नहीं। CI में शानदार।
- Exact dimensions, आसानी से। Frame को 1320 × 2868 (या कोई भी ज़रूरी size) पर size करें और वही आपका output है — कोई "App Store के लिए resize करो" वाला अंदाज़ा नहीं।
- State आप बनाते हैं। क्योंकि कोई चल रहा ऐप नहीं होता, mock data सीधे view में पास करते हैं — deterministic, कोई demo-seeding launch arguments नहीं।
- सिर्फ़ SwiftUI। Platform-backed views placeholders के रूप में रेंडर होते हैं। भरोसा करने से पहले अपनी स्क्रीन वेरिफ़ाई करें।
विकल्प 4: OSS जो ऊपर वाले को wrap करते हैं
दो ओपन-सोर्स प्रोजेक्ट बार-बार सामने आते हैं, जो पास-पास की समस्याएँ हल करते हैं।
pointfree/swift-snapshot-testing सबसे लोकप्रिय SwiftUI snapshot library है, और इसके मकसद के बारे में सटीक रहना ज़रूरी है: यह regression testing के लिए बना है, स्टोर assets बनाने के लिए नहीं। पहली रन एक reference PNG record करती है और जानबूझकर fail होती है; बाद की रन उस reference से diff करती हैं ताकि उस दिन को पकड़ा जा सके जब padding या font बदलाव आपका 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)))
}
}आप record हुई reference images को raw स्क्रीनशॉट के रूप में दोबारा उपयोग कर सकते हैं, और .image(layout: .device(...)) strategies असली device sizes पर रेंडर करती हैं — लेकिन यह टूल की strictness से टकराता है (यह pixel-perfect matches चाहता है; मशीनों के बीच antialiasing के फ़र्क flaky failures लाते हैं)। इसे अपने UI को regressions से बचाने के लिए उपयोग करें, अपनी मुख्य स्क्रीनशॉट पाइपलाइन के रूप में नहीं।
AppScreenshotKit (shitamori1272 द्वारा) ठीक इसी काम के लिए बना है। एक SwiftUI view को @AppScreenshot macro से declare करते हैं जो device classes और locales specify करता है, अपनी असली स्क्रीन को DeviceView में wrap करते हैं ताकि यह एक Apple device frame के अंदर रेंडर हो, और एक Swift Testing function से export करते हैं। यह output को एक साफ़ Screenshots/<locale>/<device>/ tree में organize करता है।
// 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 जानने लायक है: storescreens जैसे CLIs पूरी App Store Connect पाइपलाइन (parallel simulators पर XCUITest कैप्चर, captions के साथ framed renders, Apple के API से metadata upload) एक config file से चलाते हैं। अगर एक ही, idempotent, CI-friendly chain चाहिए तो ये सच में उपयोगी हैं। ये caption layout के बारे में अपनी राय भी लाते हैं — जो वह seam है जहाँ मार्केटिंग सतह के लिए एक dedicated visual editor आमतौर पर जीत जाता है।
वह हिस्सा जो इनमें से कोई नहीं करता: कंपोज़िशन + 50-भाषा लोकलाइज़ेशन
यहाँ सटीक और ईमानदार पैराग्राफ है। ऊपर हर विकल्प bare frames देता है — device resolution पर आपका app UI (या, AppScreenshotKit और इसके जैसों के साथ, एक सादे device mockup के अंदर एक frame)। जो App Store कैरोसेल convert करते हैं वे bare frames नहीं होते: वे frame एक styled device mockup के अंदर होता है, बैकग्राउंड पर, एक छोटी headline के नीचे, अक्सर multi-panel sequence के रूप में, और हर उस locale में repeat होता है जिसे आप target करते हैं। यह कंपोज़िशन चरण Mokbi के लिए है। आप जो frames कैप्चर किए हैं (XCUITest, ImageRenderer, simulator, कहीं से भी) वे browser editor में लाएं, डिवाइस फ़्रेम, कैप्शन और बैकग्राउंड जोड़ें, लगभग एक क्लिक में 50 App Store languages तक captions translate करें, और हर ज़रूरी size पर batch-export करें। डिज़ाइन watermarked preview के साथ मुफ्त है; बिना सीमा के export और publishing एक subscription के साथ आते हैं — Solo €29.99/mo (1 app) या Studio €49.99/mo (5 apps तक), कोई one-time purchase नहीं। सीमा के बारे में स्पष्ट रहें: Mokbi source screenshots कैप्चर नहीं करता — यह आपका ऐप नहीं चलाता या आपके views रेंडर नहीं करता। Capture ऊपर बताए नेटिव Swift tooling के साथ रहता है; Mokbi उस tooling के output को कंपोज़ और लोकलाइज़ करता है।
एक व्यावहारिक संयुक्त SwiftUI वर्कफ़्लो
- सोर्स फ्रेम नेटिवली कैप्चर करें। Deterministic SwiftUI स्क्रीन के लिए, उन्हें
ImageRendererसे exact pixel sizes पर रेंडर करें (विकल्प 3) — यह सबसे तेज़ है और simulator नहीं चाहिए। Platform views वाली स्क्रीन के लिए, या जहाँ असली live state चाहिए, एक XCUITest (विकल्प 1) या Swift Testing (विकल्प 2) कैप्चर लिखें और 6.9-inch iPhone class और 13-inch iPad class simulator पर चलाएं। - ज़रूरत हो तो कैप्चर लोकलाइज़ करें। प्रति भाषा एक test-plan configuration जोड़ें ताकि ऐप UI हर locale में रेंडर हो, या
ImageRendererको दी जाने वाली view में locale पास करें। यह app pixels को लोकलाइज़ करता है — आपकी मार्केटिंग headline को नहीं। - मार्केटिंग कैरोसेल कंपोज़ करें। Frames Mokbi में डालें, styled frames/captions/backgrounds जोड़ें, multi-panel sequence बनाएं। यह वह चरण है जो नेटिव tooling नहीं कर सकती।
- Captions translate करें और एक्सपोर्ट करें। Headline copy को अपने target App Store locales में one-click translate करें, फिर हर ज़रूरी dimension batch-export करें।
- अगले UI बदलाव पर, दोबारा कैप्चर करें और दोबारा खोलें। Renderer या UI test फिर चलाएं, सेव किया हुआ Mokbi project फिर खोलें, frames बदलें, दोबारा एक्सपोर्ट करें। कंपोज़िशन और translations सुरक्षित रहते हैं।
ऑटोमेशन पूरी तरह कब छोड़ सकते हैं
अगर आप साल में एक से तीन release शिप करते हैं, तो कोई भी screenshot harness बनाना — नेटिव हो या Fastlane — इससे बचाए घंटों से ज़्यादा घंटे खर्च कर देगा। सही device class पर iOS Simulator में ऐप चलाएं, हर स्क्रीन पर जाएं, simulator की screenshot command दबाएं (यह exact device resolution पर कैप्चर करती है), और दस मिनट में App Store-ready PNGs मिल जाते हैं। फिर एक बार कंपोज़ और लोकलाइज़ करें और आगे बढ़ें। ऊपर बताया automation ठीक तब फ़ायदेमंद होता है जब "हाथ से दोबारा शूट करो" दस मिनट का काम नहीं रहता — बार-बार releases, कई locales, या portfolio में कई apps। Machinery को release cadence के हिसाब से चुनें, न कि जो rigorous लगे उसके हिसाब से। यही logic दूसरे platforms पर भी लागू होती है; देखें React Native में यही समस्या और व्यापक Fastlane बनाम no-code trade-off।
जो भी tool से कैप्चर करें, export से पहले targets की पुष्टि करें: exact pixel dimensions और format rules (PNG/JPEG, RGB, no alpha channel) App Store स्क्रीनशॉट sizes guide से लें। Apple एक pixel के फ़र्क पर dimensions बिना किसी tolerance के reject कर देता है, इसलिए यह तीस सेकंड लगाने लायक है।