
Ios Marketing Capture Automation
- 971 installs
- 6 repo stars
- Updated July 18, 2026
- aradotso/marketing-skills
ios-marketing-capture-automation is an agent skill that automates multi-locale App Store screenshot capture and widget PNG rendering for SwiftUI iOS apps using in-app capture systems and demo data seeding.
About
ios-marketing-capture-automation is a marketing skill from aradotso/marketing-skills by ara.so that automates localized App Store marketing asset production for SwiftUI iOS applications. The skill sets up an in-app capture system, seeds demo data, and renders UI elements and widgets as isolated PNG images across all supported locales. Developers reach for ios-marketing-capture-automation when App Store submission deadlines require consistent screenshots in every language without manual Simulator exports. Trigger phrases include capture marketing screenshots, generate locale screenshots, render widgets as PNGs, and automate App Store screenshot capture for SwiftUI apps.
- Adds DEBUG-gated in-app capture system
- Seeds deterministic demo data
- Renders widgets at 3x with transparency
- Loops all locales in one build via -AppleLanguages
Ios Marketing Capture Automation by the numbers
- 971 all-time installs (skills.sh)
- +3 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #447 of 1,881 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aradotso/marketing-skills --skill ios-marketing-capture-automationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 971 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 18, 2026 |
| Repository | aradotso/marketing-skills ↗ |
How do you automate App Store screenshots for all locales?
Install this skill to automatically generate localized App Store screenshots and widget renders for SwiftUI iOS apps.
Who is it for?
iOS developers shipping SwiftUI apps who need automated multi-locale App Store screenshots and widget marketing assets without manual capture.
Skip if: Android developers, backend API teams, or iOS projects without SwiftUI marketing capture requirements.
When should I use this skill?
The task involves App Store screenshots, multi-locale capture, SwiftUI widget PNG renders, or automated iOS marketing asset generation.
What you get
Localized App Store screenshot sets, isolated widget PNG renders, and seeded demo-state marketing images per locale.
- Localized App Store screenshots
- Widget PNG renders
- Demo-seeded marketing images
Files
iOS Marketing Capture Automation
Skill by ara.so — Marketing Skills collection.
What This Skill Does
This skill helps you automate marketing screenshot capture for SwiftUI iOS apps by building an in-app capture system that:
- Adds a
#if DEBUG-gated capture system with zero production footprint - Seeds deterministic demo data so screenshots look populated and polished
- Navigates to each screen programmatically via step-based coordinator
- Snapshots full window including status bar, safe area, and presented sheets
- Renders isolated elements (cards, widgets, charts) via
ImageRendererat 3x with transparency - Loops every locale automatically — one build, N relaunches with
-AppleLanguages - Works with any SwiftUI navigation:
TabView,NavigationStack,NavigationSplitView
Installation
Using npx skills (recommended)
npx skills add ParthJadhav/ios-marketing-captureGlobal install (available across all projects):
npx skills add ParthJadhav/ios-marketing-capture -gAgent-specific install:
npx skills add ParthJadhav/ios-marketing-capture -a claude-codeManual installation
git clone https://github.com/ParthJadhav/ios-marketing-capture ~/.claude/skills/ios-marketing-captureRequirements Checklist
Before starting, verify:
- ✅ Xcode 16+ (synchronized folder groups support)
- ✅ iOS 17+ deployment target (for
ImageRenderer,@Observable) - ✅ A simulator runtime matching target iOS version
- ✅ Python 3 (for JSON parsing in shell script)
- ✅ SwiftUI-based app with defined navigation structure
Core Concepts
In-App Capture (Not XCUITest)
This approach uses in-app capture instead of XCUITest/Fastlane because:
- No test target needed — many projects lack one, adding means fragile pbxproj edits
- Direct access — ViewModels, SwiftData,
ImageRenderer,UIWindow.drawHierarchy - Faster —
xcodebuild buildonce, thensimctl launchper locale - Element renders require it —
ImageRenderermust run inside app process
Step-Based Coordinator
Each screenshot is a self-contained CaptureStep:
struct CaptureStep {
let name: String // "01-home"
let navigate: @MainActor () -> Void // put app in right state
let settle: Duration // wait for animations
let cleanup: (@MainActor () -> Void)? // tear down before next step
}Implementation Pattern
1. Gather Requirements
When user asks to capture screenshots, collect:
1. Screens to capture — exact tab names or navigation paths 2. Elements to render — cards, widgets, charts to isolate 3. Locales — explicit list or "all locales in xcstrings" 4. Device — simulator model (e.g. "iPhone 17") 5. Appearance — light, dark, or both 6. Seed data requirements — what demo data needs to populate
2. Generated File Structure
Create this structure:
YourApp/
├── Debug/
│ └── MarketingCapture.swift # Capture system (DEBUG-only)
├── ContentView.swift # Modified — DEBUG hook
scripts/
└── capture-marketing.sh # Build + locale loopOutput lands in:
marketing/
en/
01-home.png
02-detail.png
elements/
card-item.png
widget-small.png
de/
01-home.png
...3. Core Capture System Code
#if DEBUG
import SwiftUI
struct CaptureStep {
let name: String
let navigate: @MainActor () -> Void
let settle: Duration
let cleanup: (@MainActor () -> Void)?
}
@Observable
final class MarketingCaptureCoordinator {
var isCapturing = false
var currentStep = 0
private let steps: [CaptureStep]
private let outputDir: URL
private let elementsDir: URL
init(steps: [CaptureStep], locale: String) {
self.steps = steps
let base = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("marketing/\(locale)")
self.outputDir = base
self.elementsDir = base.appendingPathComponent("elements")
try? FileManager.default.createDirectory(
at: outputDir,
withIntermediateDirectories: true
)
try? FileManager.default.createDirectory(
at: elementsDir,
withIntermediateDirectories: true
)
}
@MainActor
func startCapture() async {
isCapturing = true
currentStep = 0
for step in steps {
step.navigate()
try? await Task.sleep(for: step.settle)
captureWindow(name: step.name)
step.cleanup?()
currentStep += 1
}
isCapturing = false
print("✅ Capture complete: \(outputDir.path)")
exit(0)
}
private func captureWindow(name: String) {
guard let window = UIApplication.shared
.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first?.windows.first else { return }
let renderer = UIGraphicsImageRenderer(bounds: window.bounds)
let image = renderer.image { ctx in
window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
}
let url = outputDir.appendingPathComponent("\(name).png")
try? image.pngData()?.write(to: url)
}
}
struct MarketingElementHarness {
@MainActor
static func renderElement<Content: View>(
name: String,
width: CGFloat,
cornerRadius: CGFloat,
background: Color,
@ViewBuilder content: () -> Content
) {
let renderer = ImageRenderer(content: content()
.frame(width: width)
.background(background)
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
)
renderer.scale = 3.0
guard let uiImage = renderer.uiImage else { return }
let base = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
.deletingLastPathComponent()
.deletingLastPathComponent()
let locale = Locale.current.language.languageCode?.identifier ?? "en"
let elementsDir = base
.appendingPathComponent("marketing/\(locale)/elements")
try? FileManager.default.createDirectory(
at: elementsDir,
withIntermediateDirectories: true
)
let url = elementsDir.appendingPathComponent("\(name).png")
try? uiImage.pngData()?.write(to: url)
}
}
#endif4. Navigation Pattern Examples
TabView Navigation
@Observable
final class AppCoordinator {
var selectedTab = 0
func setTab(_ index: Int) {
selectedTab = index
}
}
// In MarketingCapture.swift:
func makeSteps(coordinator: AppCoordinator) -> [CaptureStep] {
[
CaptureStep(
name: "01-home",
navigate: { coordinator.setTab(0) },
settle: .seconds(0.5),
cleanup: nil
),
CaptureStep(
name: "02-shelf",
navigate: { coordinator.setTab(1) },
settle: .seconds(0.5),
cleanup: nil
)
]
}NavigationStack with Router
@Observable
final class Router {
var path: [Route] = []
func push(_ route: Route) {
path.append(route)
}
func popToRoot() {
path.removeAll()
}
}
// Steps:
CaptureStep(
name: "03-detail",
navigate: {
router.popToRoot()
router.push(.coffeeDetail(coffee))
},
settle: .seconds(0.8),
cleanup: { router.popToRoot() }
)NavigationSplitView
@Observable
final class SplitCoordinator {
var sidebarSelection: SidebarItem?
var detailSelection: DetailItem?
}
// Steps:
CaptureStep(
name: "04-settings",
navigate: {
coordinator.sidebarSelection = .settings
coordinator.detailSelection = nil
},
settle: .seconds(0.6),
cleanup: nil
)5. Demo Data Seeding
#if DEBUG
@MainActor
func seedMarketingData(modelContext: ModelContext) {
// Clear existing
try? modelContext.delete(model: Coffee.self)
// Seed deterministic data
let coffees = [
Coffee(
name: "Morning Blend",
roaster: "Blue Bottle",
origin: "Ethiopia",
notes: ["Blueberry", "Chocolate", "Citrus"],
roastDate: Date().addingTimeInterval(-7 * 86400)
),
Coffee(
name: "Dark Horse",
roaster: "Stumptown",
origin: "Colombia",
notes: ["Caramel", "Nuts", "Brown Sugar"],
roastDate: Date().addingTimeInterval(-3 * 86400)
)
]
coffees.forEach { modelContext.insert($0) }
try? modelContext.save()
}
#endif6. Integrating with ContentView
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@State private var coordinator = AppCoordinator()
#if DEBUG
@State private var captureCoordinator: MarketingCaptureCoordinator?
#endif
var body: some View {
TabView(selection: $coordinator.selectedTab) {
HomeView()
.tag(0)
.tabItem { Label("Home", systemImage: "house") }
ShelfView()
.tag(1)
.tabItem { Label("Shelf", systemImage: "books.vertical") }
}
#if DEBUG
.task {
if ProcessInfo.processInfo.environment["MARKETING_CAPTURE"] == "1" {
seedMarketingData(modelContext: modelContext)
let locale = Locale.current.language.languageCode?.identifier ?? "en"
captureCoordinator = MarketingCaptureCoordinator(
steps: makeMarketingSteps(coordinator: coordinator),
locale: locale
)
try? await Task.sleep(for: .seconds(1))
await captureCoordinator?.startCapture()
}
}
#endif
}
}7. Element Rendering Examples
Widget Rendering
#if DEBUG
@MainActor
func renderWidgets() {
let entry = PulseWidgetEntry(
date: Date(),
coffee: seedCoffee,
recentBrew: seedBrew
)
// Small widget
MarketingElementHarness.renderElement(
name: "widget-pulse-small",
width: 158,
cornerRadius: 16,
background: .white
) {
PulseWidgetSmallView(entry: entry)
.padding(16) // WidgetKit padding manually added
}
// Medium widget
MarketingElementHarness.renderElement(
name: "widget-pulse-medium",
width: 338,
cornerRadius: 16,
background: .white
) {
PulseWidgetMediumView(entry: entry)
.padding(16)
}
}
#endifCard Rendering
#if DEBUG
@MainActor
func renderCards(coffees: [Coffee]) {
for (index, coffee) in coffees.enumerated() {
MarketingElementHarness.renderElement(
name: "card-\(index + 1)",
width: 380,
cornerRadius: 20,
background: Color(.systemBackground)
) {
CoffeeCard(coffee: coffee)
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
}
#endifChart Rendering
#if DEBUG
@MainActor
func renderCharts() {
MarketingElementHarness.renderElement(
name: "chart-cupping",
width: 380,
cornerRadius: 16,
background: Color(.systemBackground)
) {
CuppingRadarChart(scores: sampleScores)
.frame(height: 300)
.padding(20)
}
}
#endif8. Build and Launch Script
#!/bin/bash
# scripts/capture-marketing.sh
set -e
APP_NAME="YourApp"
SCHEME="YourApp"
DEVICE="iPhone 17"
IOS_VERSION="18.2"
LOCALES=("en" "de" "es" "fr" "ja")
SIMULATOR_ID=$(xcrun simctl list devices available | \
grep "$DEVICE ($IOS_VERSION)" | head -1 | \
grep -o '\[.*\]' | tr -d '[]')
if [ -z "$SIMULATOR_ID" ]; then
echo "❌ Simulator '$DEVICE ($IOS_VERSION)' not found"
exit 1
fi
echo "📱 Using simulator: $SIMULATOR_ID"
# Boot simulator
xcrun simctl boot "$SIMULATOR_ID" 2>/dev/null || true
sleep 2
# Build once
echo "🔨 Building..."
xcodebuild \
-scheme "$SCHEME" \
-destination "id=$SIMULATOR_ID" \
-configuration Debug \
-derivedDataPath ./build \
build
APP_PATH=$(find ./build -name "${APP_NAME}.app" | head -1)
BUNDLE_ID=$(defaults read "$APP_PATH/Info.plist" CFBundleIdentifier)
echo "📦 Bundle ID: $BUNDLE_ID"
# Install once
xcrun simctl install "$SIMULATOR_ID" "$APP_PATH"
# Per-locale capture
for LOCALE in "${LOCALES[@]}"; do
echo ""
echo "📸 Capturing $LOCALE..."
xcrun simctl launch \
--terminate-running-process \
"$SIMULATOR_ID" \
"$BUNDLE_ID" \
-AppleLanguages "($LOCALE)" \
-MARKETING_CAPTURE 1
sleep 8
done
# Copy to project
CONTAINER=$(xcrun simctl get_app_container "$SIMULATOR_ID" "$BUNDLE_ID" data)
cp -R "$CONTAINER/Documents/../../tmp/marketing" ./marketing/
echo ""
echo "✅ All locales captured → ./marketing/"Critical Gotchas (Baked Into Skill)
1. Live Activities Persist Across Launches
Problem: Next locale crashes on stale SwiftData references.
Solution: End all Live Activities in cleanup:
for activity in Activity<PulseAttributes>.activities {
await activity.end(nil, dismissalPolicy: .immediate)
}2. Re-Seeding Per Locale
Problem: CloudKit sync churn causes crashes.
Solution: Seed once at launch, not per step.
3. ViewModels Setup Before Seed
Problem: VMs hold stale empty snapshots.
Solution: Seed data → wait 1s → instantiate VMs → capture.
4. Setting Trigger Binding to nil
Problem: Doesn't dismiss fullScreenCover — captures wrong sheet.
Solution: Use dedicated dismiss closure:
.fullScreenCover(isPresented: $showTimer) {
TimerView(onDismiss: { showTimer = false })
}5. NavigationPath Can't Be Popped Externally
Problem: Pushing over existing path captures wrong stack.
Solution: Always popToRoot() before push() in navigate block.
6. membershipExceptions Is an INCLUSION List
Problem: Widget target membership goes backwards — widget renders fail.
Solution: Set membershipExceptions for DEBUG files to exclude widget target:
// In pbxproj or via Xcode:
// MarketingCapture.swift → Target Membership → Widget ❌7. ImageRenderer + ProgressView
Problem: Renders as prohibited symbol without explicit style.
Solution: Force .circular style:
ProgressView()
.progressViewStyle(.circular)8. .containerBackground Outside WidgetKit
Problem: No-op — widget renders have no background.
Solution: Manually add background in render harness:
WidgetView(entry: entry)
.background(Color.white)9. iPhone 8 Plus Gone on iOS 18+
Problem: Legacy 6.5" simulator unavailable.
Solution: Use iPhone 17 Pro Max or latest available large device.
10. Locale Launch Argument Format
Problem: Locale ignored if parens missing.
Solution: Always use "($LOCALE)" format:
-AppleLanguages "(de)" # ✅
-AppleLanguages "de" # ❌11. SwiftUI Animations in ImageRenderer
Problem: Captures frame 0, not animated state.
Solution: Render non-animated state or use explicit .animation(nil).
Common Workflows
Capturing Timer Mid-Countdown
CaptureStep(
name: "05-timer-active",
navigate: {
router.popToRoot()
coordinator.startTimer(seconds: 180)
// Timer view subscribes to coordinator.activeTimer
},
settle: .seconds(1.5), // Let timer animate in
cleanup: {
coordinator.stopTimer()
router.popToRoot()
}
)Capturing Presented Sheet
@State private var showSheet = false
// Step:
CaptureStep(
name: "06-add-coffee",
navigate: {
showSheet = true
},
settle: .seconds(0.8),
cleanup: {
showSheet = false
}
)
// View:
.sheet(isPresented: $showSheet) {
AddCoffeeView()
}Rendering All Widgets
#if DEBUG
@MainActor
func renderAllWidgets() {
let entries = [
("pulse-small", PulseWidgetSmallView.self, 158),
("pulse-medium", PulseWidgetMediumView.self, 338),
("pulse-large", PulseWidgetLargeView.self, 338)
]
for (name, viewType, width) in entries {
MarketingElementHarness.renderElement(
name: "widget-\(name)",
width: width,
cornerRadius: 16,
background: .white
) {
viewType.init(entry: sampleEntry)
.padding(16)
}
}
}
#endifTroubleshooting
Capture exits immediately
Check: MARKETING_CAPTURE=1 env var is set in launch args.
xcrun simctl launch ... -MARKETING_CAPTURE 1Locale not applied
Check: Parens in -AppleLanguages:
-AppleLanguages "(de)" # ✅ CorrectWidget renders blank
Check: Manual padding added (WidgetKit adds 16pt automatically):
WidgetView(entry: entry)
.padding(16) // Add thisScreenshot captures wrong screen
Check: Settle duration long enough for animations:
settle: .seconds(0.8) // Increase if neededSwiftData crash on second locale
Check: Live Activities ended in cleanup:
cleanup: {
Task {
for activity in Activity<Attrs>.activities {
await activity.end(nil, dismissalPolicy: .immediate)
}
}
}Element renders with wrong size
Check: Frame width explicitly set:
content()
.frame(width: 380) // Must be explicitConfiguration Reference
Capture Step Properties
| Property | Type | Purpose |
|---|---|---|
name | String | Filename without extension (e.g. "01-home") |
navigate | @MainActor () -> Void | Put app in correct state |
settle | Duration | Wait time for animations |
cleanup | (@MainActor () -> Void)? | Tear down before next step |
Script Variables
| Variable | Example | Purpose |
|---|---|---|
APP_NAME | "YourApp" | App target name |
SCHEME | "YourApp" | Xcode scheme |
DEVICE | "iPhone 17" | Simulator model |
IOS_VERSION | "18.2" | iOS runtime |
LOCALES | ("en" "de" "es") | Locale codes |
Environment Variables
Set in launch args or script:
MARKETING_CAPTURE=1 # Enable capture modePost-Processing
Use app-store-screenshots to composite captured PNGs into Apple-style marketing pages with device mockups, headlines, and gradients.
npx skills add ParthJadhav/app-store-screenshotsThen prompt:
Composite my marketing screenshots into App Store pages with device framesWhen to Use This Skill
✅ Use when:
- Capturing marketing screenshots for App Store
- Rendering isolated components (cards, widgets, charts)
- Multi-locale asset generation
- SwiftUI-based iOS app with defined navigation
- Need full control over demo data and app state
❌ Don't use when:
- UIKit-based app (requires different capture approach)
- XCUITest infrastructure already working well
- Single locale, manual capture sufficient
- App uses web views or external content (can't seed)
Example: Complete Coffee App Capture
#if DEBUG
@MainActor
func makeMarketingSteps(
coordinator: AppCoordinator,
router: Router,
viewModel: HomeViewModel
) -> [CaptureStep] {
let coffees = viewModel.coffees
return [
// Tab 1: Home
CaptureStep(
name: "01-home",
navigate: { coordinator.setTab(0) },
settle: .seconds(0.5),
cleanup: nil
),
// Tab 2: Shelf
CaptureStep(
name: "02-shelf",
navigate: { coordinator.setTab(1) },
settle: .seconds(0.5),
cleanup: nil
),
// Detail: First coffee
CaptureStep(
name: "03-detail",
navigate: {
coordinator.setTab(0)
router.popToRoot()
router.push(.coffeeDetail(coffees[0]))
},
settle: .seconds(0.8),
cleanup: { router.popToRoot() }
),
// Timer: Active brew
CaptureStep(
name: "04-timer",
navigate: {
coordinator.setTab(0)
router.popToRoot()
coordinator.startTimer(seconds: 180)
},
settle: .seconds(1.2),
cleanup: {
coordinator.stopTimer()
router.popToRoot()
}
),
// Settings
CaptureStep(
name: "05-settings",
navigate: { coordinator.setTab(2) },
settle: .seconds(0.5),
cleanup: nil
),
// Elements: Cards
CaptureStep(
name: "elements",
navigate: {
for (index, coffee) in coffees.enumerated() {
MarketingElementHarness.renderElement(
name: "card-\(index + 1)",
width: 380,
cornerRadius: 20,
background: Color(.systemBackground)
) {
CoffeeCard(coffee: coffee)
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
renderAllWidgets()
},
settle: .seconds(0.1),
cleanup: nil
)
]
}
#endifRun:
chmod +x scripts/capture-marketing.sh
./scripts/capture-marketing.shOutput:
marketing/
en/
01-home.png
02-shelf.png
03-detail.png
04-timer.png
05-settings.png
elements/
card-1.png
card-2.png
widget-pulse-small.png
widget-pulse-medium.png
de/
[same structure]
es/
[same structure]
fr/
[same structure]
ja/
[same structure]Summary
This skill automates iOS marketing screenshot capture by:
1. Gathering requirements — screens, elements, locales, device, appearance 2. Generating capture system — DEBUG-gated coordinator + steps 3. Seeding demo data — deterministic, locale-agnostic 4. Navigating programmatically — TabView, NavigationStack, or SplitView 5. Capturing window — drawHierarchy for full screenshots 6. Rendering elements — ImageRenderer for isolated components 7. Looping locales — simctl launch with -AppleLanguages
The result: one build, N launches, complete multi-locale marketing assets ready for App Store or further compositing.
Related skills
How it compares
Choose this over manual Simulator screenshots when a SwiftUI app must ship localized App Store assets and widget renders at scale.
FAQ
What does ios-marketing-capture-automation produce?
ios-marketing-capture-automation generates multi-locale App Store screenshots and isolated widget PNG renders for SwiftUI iOS apps. The skill uses in-app capture, demo data seeding, and element rendering instead of manual Simulator exports.
Which iOS projects fit this marketing capture skill?
ios-marketing-capture-automation targets SwiftUI iOS apps needing localized App Store assets. Developers invoke it when tasks mention locale screenshots, widget PNG renders, or automated App Store capture across languages.