iOS Marketing Capture
概要
SwiftUI iOSアプリのマーケティング用スクリーンショットを、複数のロケールにわたって再現可能な形で自動取得します。以下の2つの並列出力ストリームを提供します:
- 全画面キャプチャ — マーケティングに関連するすべての画面を、決定論的なシードデータ、実際のステータスバー/セーフエリア付きでキャプチャ
- 要素キャプチャ — 特定のコンポーネント(カード、ウィジェット、チャートなど)を任意のスケールで独立してレンダリング。角丸の内側は自然な背景、外側は透過
このスキルはキャプチャステップです。ユーザーがAppleスタイルのマーケティングページ(デバイスモックアップ、見出し、グラデーション)をショットの周りに合成したい場合は、後処理ステップとしてapp-store-screenshotsスキルと組み合わせてください。
コアアプローチ
アプリ内キャプチャモードを使用します。XCUITestは使用しません。これは、Fastlane snapshot / XCUITestの慣習に対する意図的な選択であり、ほぼすべての実プロジェクトで有効です。
XCUITestではなくアプリ内キャプチャを選ぶ理由:
- 新しいテストターゲットが不要。 既存のXcodeプロジェクトにUIテストターゲットを追加するのは、脆弱なpbxprojの修正作業です。多くのプロジェクトにはテストターゲットがなく、xcodegenも使用していません — 手動で追加するのはエラーが発生しやすいです。
- 高速な反復。 UIテストは実行ごとに30秒以上かかります。アプリ内キャプチャは、インストール済みバイナリの再起動だけです。
xcodebuild test不要。 フロー全体は、xcodebuild buildを1回実行し、その後ロケールごとにsimctl launchするだけです。テストバンドルのオーバーヘッドがありません。- 実際のアプリ状態にアクセス可能。 ViewModel、SwiftData、ImageRenderer、
UIWindow.drawHierarchyを直接呼び出せます。XCUITestではタップとアクセシビリティ要素の読み取りしかできません。 - 要素レンダリングはプロセス内でしか動作しない。 ウィジェットビューや独立したコンポーネントに対する
ImageRendererは、アプリプロセス内部で実行する必要があります — XCUITestに相当するものはありません。
仕組み:
- DEBUG専用の
MarketingCapture.swiftファイルがメインアプリターゲットに存在します -MarketingCapture 1で起動すると、アプリがデータをシードし、コーディネーターがCaptureStepのリストを順に処理します — 各ステップはナビゲート、セトル待ち、スナップショット、クリーンアップを行います- PNGファイルはアプリのサンドボックス
Documents/marketing/<locale>/ディレクトリに書き込まれます - シェルスクリプトが1回ビルドし、インストールし、
-AppleLanguages (xx) -AppleLocale xxで再起動することでロケールをループし、simctl get_app_containerでファイルを取り出します
プロセス
以下の手順を順番に実行してください。先に進まないでください。
ステップ 1: 要件の収集
ユーザーに以下の質問を一度に1つずつ行ってください(まとめて質問しないでください — 各回答が後続の質問を無効にする可能性があります):
- キャプチャする画面 — 「どの画面をキャプチャしますか?各画面のナビゲーションパスまたはタブ名を教えてください。」「主要なフロー」ではなく、具体的なリストを取得します。
- 独立した要素 — 「透過背景で独立してレンダリングしたいコンポーネントはありますか?(カルーセルカード、ウィジェット、ヒーロータイル、チャートなど)」
- ロケール — 「どのロケールですか?(a)
Localizable.xcstrings内のすべてのロケール、(b) 私が指定するApp Storeサブセット、(c) 明示的なリストを提供する。」(a)の場合、.xcstringsファイルをgrepしてロケールコードを取得します:python3 -c "import json; d=json.load(open('<path>/Localizable.xcstrings')); langs=set(); [langs.update(v.get('localizations',{}).keys()) for v in d['strings'].values()]; print(sorted(langs))" - デバイス — 「どのシミュレータを使用しますか?(iOS 26のデザイン機能には6.1インチiPhone 17を推奨)」 —
xcrun simctl list devices availableでデバイスが利用可能か確認します。 - 外観モード — 「ライトのみ、ダークのみ、または両方?」
- シードデータ — 「現在、デモデータはどのように投入されていますか?(a) 新規インストール時に自動でシードされる、(b) デバッグ用の「デモデータを読み込む」ボタンがある、(c) 手動で追加している、(d) デモデータはまだ存在しない。」その後:「既存のデータは、リストアップされたすべての画面がマーケティング用に十分に表示されるほど網羅的ですか?ユーザーと一緒に監査します。」
ステップ 2: 探索
コードを書く前に、コードベースを十分に探索して以下を確認します:
- プロジェクトはXcode同期フォルダグループ(Xcode 16以降、
PBXFileSystemSynchronizedRootGroup)を使用していますか?はいの場合、新しいファイルは自動的にターゲットに含まれます — pbxprojの編集は不要です。grep -c PBXFileSystemSynchronized <proj>.xcodeproj/project.pbxprojで確認します。 - ルートナビゲーションパターンは?
TabView(selection:)— 最も一般的。必要なもの:@State selectedTabバインディング、タブインデックス、ネストされたNavigationStackを持つタブ。NavigationStack(ルーターを持つ単一スタック) — 必要なもの:パスバインディングまたはルーターオブジェクト、NavigationLink(value:)/.navigationDestinationタイプのセット。NavigationSplitView— 必要なもの:サイドバー選択バインディング、詳細列のナビゲーション状態。- カスタムコーディネーター / UIKitホスト — 必要なもの:コーディネーターの
navigate(to:)メソッドまたは同等のもの。
- ディープリンクはどのようにルーティングされますか?
onOpenURLハンドラーと、URLをナビゲーション状態にマッピングするenum/switchを見つけます。 - デモデータシーダーはどこで定義されていますか?デバッグボタン(ある場合)から、実際に
ModelContextに書き込む関数までのコードパスをトレースします。シーダーが存在しない場合、以下の「デモデータシーダーの作成」を参照してください。 - ウィジェットは別のターゲットにありますか?ウィジェットビューファイルとエントリタイプはメインアプリターゲットにもありますか?(ほぼ間違いなくありません — ImageRendererでレンダリングするには追加する必要があります。)
- アプリはLive Activities / ActivityKitを使用していますか?はいの場合、既知の落とし穴としてフラグを立てます(以下参照)。
- アプリはSwiftData + CloudKit同期(
cloudKitDatabase: .automatic)を使用していますか?はいの場合、既知の落とし穴としてフラグを立てます。 - キャプチャするビューがデフォルト以外の状態にある必要がありますか?(例:カウントダウン中のタイマー、部分的に入力されたフォーム、特定の値を持つチャート)。はいの場合、それぞれに
static varプライミングメカニズムが必要です(以下の「ビュー状態のプライミング」参照)。
ステップ 3: 設計をユーザーに提示
コードを書く前に、以下の構造で計画を要約します。進める前に明示的な承認を得てください:
- アーキテクチャ(アプリ内キャプチャモード、単一ファイル、DEBUGゲート)
- ファイルリスト(作成/変更する正確なパス)
- 画面ごとのキャプチャ計画(各画面にどのように到達するか — タブインデックス、ナビゲーションパス、シートトリガー)
- キャプチャ順序の根拠(どの画面が他の画面より先に来る必要があるか — 落とし穴5参照)
- 要素レンダリングのアプローチ(どのコンポーネント、どのようにラップするか)
- 出力レイアウト(フォルダ構造、命名規則)
- このプロジェクトに関連する既知の落とし穴(ステップ2でフラグを立てたもの)
- プライミングされた状態(どのビュー、どのstatic変数)
ステップ 4: 実装
templates/内のテンプレートを出発点として使用します。これらはコピーペースト可能な雛形ではなく、参考パターンです — すべてのプロジェクトでナビゲーション、モデル、ビューが異なります。テンプレートは構成要素を示しており、対象アプリ用に組み立てます。
作成する主要ファイル:
<AppName>/Debug/MarketingCapture.swift— キャプチャシステム全体、DEBUGのみ。以下を含む:MarketingCaptureenum(起動引数の解析、出力ヘルパー、ウィンドウスナップショット、プライミング変数)MarketingCaptureCoordinatorクラス([CaptureStep]を順に処理し、各ステップをスナップショット)MarketingElementHarnessenum(カード、ウィジェット、チャートのImageRendererレンダリング)
<AppName>/ContentView.swift(またはルートビューがある場所) — データをシードし、コーディネーターを実行するDEBUGフック。- プライミングされた状態が必要なビュー — DEBUGゲート付きの
.onAppearフックと.onReceiveディスミスリスナー。 scripts/capture-marketing.sh— ビルド + インストール + ロケールごとのループ。.gitignore—marketing/を追加。
ステップ 5: 段階的に検証
スクリプトをユーザーに渡して待つのはしないでください。シミュレータに対して自分で実行し、完了と宣言する前に少なくとも1つのロケールを検証します。出力されたPNGをReadツールで読み取り、各画面が期待通りに表示されているかを視覚的に確認します。一般的な実行時問題は、以下の「既知の落とし穴」にリストされています。
問題を見つけた場合は、修正し、スクリプト全体を再実行し(失敗したロケールだけでなく — 修正が以前のロケールを後退させる可能性があります)、視覚的に再検証します。
アーキテクチャ: Step-Based Capture
コーディネーターは、CaptureStep値のリストを順に処理することでキャプチャを駆動します。各ステップは自己完結型です:画面へのナビゲート方法、待機時間、後処理方法を認識しています。
struct CaptureStep {
let name: String // 出力ファイル名、例:"01-home"
let navigate: @MainActor () -> Void // アプリを適切な状態にする
let settle: Duration // アニメーション/読み込みを待つ時間
let cleanup: (@MainActor () -> Void)? // 次のステップの前に後処理
}
コーディネーターは単純なループです:
for step in steps {
step.navigate()
try? await Task.sleep(for: step.settle)
if let image = MarketingCapture.snapshotKeyWindow() {
MarketingCapture.writePNG(image, name: step.name)
}
step.cleanup?()
try? await Task.sleep(for: .milliseconds(400)) // クリーンアップアニメーション
}
異なるナビゲーションパターン用のステップ構築
TabViewアプリ(最も一般的):
// 単純なタブ切り替え — インデックスを設定するだけ
CaptureStep(name: "01-home", navigate: { setTab(0) }, settle: .milliseconds(1800), cleanup: nil)
// タブ + 表示シート
CaptureStep(
name: "05-timer-setup",
navigate: {
setTab(3)
pendingBrewRecipe = someRecipe
},
settle: .milliseconds(2000),
cleanup: {
NotificationCenter.default.post(name: MarketingCapture.dismissSheetNotification, object: nil)
pendingBrewRecipe = nil
}
)
NavigationStack + ルーターアプリ:
// スタックにルートをプッシュ
CaptureStep(
name: "02-detail",
navigate: { router.push(.itemDetail(item)) },
settle: .milliseconds(1800),
cleanup: { router.popToRoot() }
)
NavigationSplitViewアプリ:
// サイドバーアイテムを選択し、次に詳細を選択
CaptureStep(
name: "03-detail",
navigate: {
sidebarSelection = .recipes
detailSelection = recipes.first
},
settle: .milliseconds(1800),
cleanup: { detailSelection = nil }
)
順序付け: スタッキングルール
同じスタックにプッシュする画面の前に、「クリーンな」ナビゲーション状態が必要な画面をキャプチャします。 子ビュー内のネストされたNavigationPath / @Stateは、コーディネーターからポップできません。したがって:
良い: Shelf(クリーンなリスト) → Coffee Detail(Shelfのスタックにプッシュ)
悪い: Coffee Detail → Shelf(スタックに詳細が残っている)
2つの画面がNavigationStackを共有する場合、ルートレベルのビューを最初にキャプチャします。
ビュー状態のプライミング
一部の画面は、特定の非デフォルト状態でキャプチャする必要があります — カウントダウン中のタイマー、特定の値を持つチャート、半分入力されたフォーム。パターン:
-
各プライミング値に対して、
MarketingCaptureにstatic varを追加します:/// コーディネーターがタイマービューを表示する前に設定します。 /// ビューは.onAppearでこれを読み取り、特定の経過時間にジャンプします。 static var pendingElapsedSeconds: Int? /// trueに設定すると、タイマーに評価オーバーレイを表示します。 static var pendingShowAssessment: Bool = false -
対象のビューで、プライミング値を読み取るDEBUGゲート付きの
.onAppearを追加します:.onAppear { #if DEBUG if MarketingCapture.isActive, let elapsed = MarketingCapture.pendingElapsedSeconds { phase = .active timerVM.elapsedTime = TimeInterval(elapsed) timerVM.start() DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { timerVM.pause() } } #endif } -
コーディネーターで、ナビゲートする前に変数を設定します:
CaptureStep( name: "06-timer-midway", navigate: { MarketingCapture.pendingElapsedSeconds = 75 openTimerSheet(someRecipe) }, settle: .milliseconds(2400), cleanup: { MarketingCapture.pendingElapsedSeconds = nil NotificationCenter.default.post(name: MarketingCapture.dismissSheetNotification, object: nil) } )
デモデータシーダーの作成
アプリに既存のデモデータメカニズムがない場合は、作成します。<AppName>/Debug/DemoDataSeeder.swiftに配置し、#if DEBUGでラップします。
ガイドライン:
- キャプチャするすべての画面が十分にデータで満たされているように十分なデータをシードします。画面リストをシードに対して監査します。
- 現実的なコンテンツを使用:実際の地名、妥当な数値、さまざまな状態(一部のアイテムは「在庫わずか」、一部は「新鮮」、画像があるものとないものなど)。
- アプリがSwiftDataを使用する場合は、
ModelContextに直接書き込みます。Core Dataの場合は、管理オブジェクトコンテキストを使用します。RESTバックエンドの場合は、ローカルキャッシュ/ストアレイヤーを介してシードします。 - シードを冪等にします — 挿入前にデータがすでに存在するか確認します。ストアはシミュレータの再起動後も永続化され、ロケールごとに再シードするとCloudKit同期のチャーンとクラッシュが発生します。
- 異なるUI状態を満たすのに十分な多様性を含めます:空の状態は、それがマーケティング画面でない限り表示されるべきではありません。
最小限の形状:
#if DEBUG
enum DemoDataSeeder {
static func seedIfEmpty(in context: ModelContext) {
let existing = (try? context.fetchCount(FetchDescriptor<Item>())) ?? 0
guard existing == 0 else { return }
// さまざまな状態のアイテム
let items = [
Item(name: "...", status: .active, ...),
Item(name: "...", status: .lowStock, ...),
// ...すべての画面を満たすのに十分な数
]
items.forEach { context.insert($0) }
try? context.save()
}
}
#endif
要素レンダリング
要素は、角丸の外側を透過とした3xスケールのImageRendererを使用してレンダリングされます。
カード / リスト行
@MainActor
static func renderCards(items: [Item], theme: AppTheme) {
let cardWidth: CGFloat = 380
for item in items {
let card = ItemCard(item: item, theme: theme)
.padding(.horizontal, 16)
.padding(.vertical, 12)
.frame(width: cardWidth)
.background(theme.background)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
let renderer = ImageRenderer(content: card)
renderer.scale = 3
renderer.isOpaque = false
renderer.proposedSize = .init(width: cardWidth, height: nil)
if let cgImage = renderer.cgImage {
let uiImage = UIImage(cgImage: cgImage)
let filename = "marketing/\(locale)/\(theme.name)/card-\(item.id).png"
MarketingCapture.writeUIImage(uiImage, path: filename)
}
}
}
既知の落とし穴
-
SwiftData + CloudKit同期: ロケールごとにシードを再実行すると、CloudKit同期のチャーンが発生し、アプリがクラッシュする可能性があります。シーダーは必ず冪等にしてください。
-
Live Activities: アクティブなLive Activityがあるとスナップショットが汚れる場合があります。キャプチャ前に
endActivityを呼び出すか、シミュレータでLive Activityを無効にします。 -
シートのディスミス: シートをプログラムで閉じるには、
@Environment(\.dismiss)または通知パターンを使用します。コーディネーターはシートの@Stateに直接アクセスできません。 -
アラートと.confirmationDialog: スナップショットの前に、表示されているアラートや確認ダイアログが自動的に閉じられるのを待ちます(ベストエフォート)。
-
スタッキングルール違反: 画面がNavigationStackを共有する場合、ポップできないプッシュされた状態でスタックを汚染しないように、ルートレベルのビューを最初にキャプチャします。
-
シミュレータのキーボード: シミュレータで「ハードウェアキーボード」が有効になっている場合、テキストフィールドはソフトウェアキーボードを表示せず、スクリーンショットが空に見える原因になります。キャプチャ前にソフトウェアキーボードを強制表示するか、無効にします。
-
.onAppearのタイミング: 一部のビューは、データを非同期的に読み込みます。スナップショットの前に十分なsettle時間を設定するか、@State変数を使用して準備ができたことをコーディネーターに通知します。 -
ツールバーとタブバーの色: 外観モード(ライト/ダーク)を切り替えると、システムバーの色が変わる場合があります。各モードでキャプチャする場合は、一貫したスタイルを確保します。
推奨出力構造
marketing/
├── ja/
│ ├── Light/
│ │ ├── 01-home.png
│ │ ├── 02-detail.png
│ │ └── ...
│ ├── Dark/
│ │ ├── 01-home.png
│ │ ├── 02-detail.png
│ │ └── ...
│ └── elements/
│ ├── Light/
│ │ ├── card-item1.png
│ │ ├── widget-small.png
│ │ └── ...
│ └── Dark/
│ ├── card-item1.png
│ ├── widget-small.png
│ └── ...
├── en/
└── ...
テンプレート
templates/ディレクトリには、実際のプロジェクトに適合させるためのリファレンス実装が含まれています。以下を提供します:
MarketingCapture.swift.template— キャプチャシステム全体の骨格DemoDataSeeder.swift.template— サンプルシードデータcapture-marketing.sh.template— シェルスクリプトラッパー
これらのテンプレートは、プロジェクト固有の詳細(モデル名、ビューパス、タブインデックスなど)を埋める必要があるプレースホルダーコメントを含んでいます。
まとめ
このスキルは、XCUITestのボトルネックなしに、プロフェッショナルなマーケティングアセットを生成するための実用的で実績のあるアプローチです。Step-Based Captureアーキテクチャに従い、テンプレートの構成要素を適応させ、出力を段階的に検証することで、最小限の摩擦で高品質な結果を保証します。