ios-marketing-capture について
SwiftUI iOS アプリの画面や個別コンポーネントを、複数ロケールで再現可能な形で撮影するスキルです。アプリ内キャプチャを採用し、シミュレーターでのビルド、起動、画像出力まで行います。
主な機能
- 01ロケールごとの全画面撮影
- 02シードデータによる画面表示
- 03カード、ウィジェット、チャートの個別レンダリング
- 04画面遷移、待機、撮影のステップ制御
- 05複数ロケールでのビルドと出力
活用シーン
- 01ローカライズ版 App Store スクリーンショットの準備
- 02透明背景のコンポーネント画像の作成
- 03マーケティングページ用の統一された画面素材の用意
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ parthjadhav/ ios-marketing-capture --skill ios-marketing-captureインストール後、Claude Code で/ios-marketing-captureを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSECONTRIBUTING.mdREADME.md.github/FUNDING.ymlpull_request_template.md.github/ISSUE_TEMPLATE/feature_request.ymlbug_report.ymlconfig.ymltemplates/capture-marketing.sh.templateMarketingCapture.swift.template詳細
iOS マーケティング用キャプチャ
概要
複数のロケールに対応した SwiftUI iOS アプリのマーケティング用スクリーンショットを、再現性のある方法で自動キャプチャします。出力は次の 2 系統です。
- フルスクリーンキャプチャ — マーケティングに関係するすべての画面を、シード値で決定されるデータと実際のステータスバー/セーフエリアを含めてキャプチャします
- 要素キャプチャ — 特定のコンポーネント(カード、ウィジェット、グラフなど)を任意の倍率で個別にレンダリングします。角丸の内側には自然な背景を表示し、外側は透過します
このスキルはキャプチャのステップを担います。撮影画像の周囲に Apple スタイルのマーケティングページ(デバイスのモックアップ、見出し、グラデーション)を合成する場合は、後処理のステップとして app-store-screenshots スキルを組み合わせてください。
基本方針
XCUITest ではなく、アプリ内キャプチャモードを使用します。これは Fastlane snapshot/XCUITest の慣習とのトレードオフを伴う、明確な選択です。実際のほぼすべてのプロジェクトにおいて、この方法が最適です。
XCUITest よりアプリ内キャプチャを選ぶ理由:
- 新しいテストターゲットが不要です。 既存の Xcode プロジェクトに UI テストターゲットを追加するには、壊れやすい pbxproj の直接編集が必要です。テストターゲットがなく、xcodegen も使っていないプロジェクトは多く、手作業で追加すると間違いが起きやすくなります。
- 反復が速くなります。 UI テストは実行のたびに起動まで 30 秒以上かかります。アプリ内キャプチャなら、インストール済みバイナリを再起動するだけです。
xcodebuild testは不要です。 一連の処理では、最初に一度xcodebuild buildを実行し、その後はロケールごとにsimctl launchを実行します。テストバンドルによるオーバーヘッドもありません。- 実際のアプリ状態にアクセスできます。 ViewModel、SwiftData、ImageRenderer、
UIWindow.drawHierarchyを直接呼び出せます。XCUITest でできるのは、タップとアクセシビリティ要素の読み取りだけです。 - 要素のレンダリングには、いずれにしてもアプリ内実行が必要です。 ウィジェットのビューや個別のコンポーネントで
ImageRendererを使う場合は、アプリプロセス内で実行する必要があります。XCUITest には同等の機能がありません。
動作の仕組み:
- DEBUG 専用の
MarketingCapture.swiftファイルをメインアプリターゲットに配置します -MarketingCapture 1を指定してアプリを起動すると、アプリがデータをシードし、コーディネーターがCaptureStepのリストを順に処理します。各ステップで画面に移動し、表示が落ち着くのを待ち、スナップショットを撮って後処理を行います- PNG はアプリのサンドボックス内にある
Documents/marketing/<locale>/ディレクトリに書き込まれます - シェルスクリプトでビルドとインストールを一度実行した後、
-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)デバッグ用の『Load Demo Data』ボタンがある、(c)手動で追加する、(d)デモデータがまだない」のいずれかを選んでください。」続けて、「既存のデータだけで、一覧にあるすべての画面をマーケティング用に十分な内容が表示される状態にできますか?ユーザーと一緒に確認してください。」
Step 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による初期化の仕組みが必要です(下記の「ビュー状態の初期化」を参照)。
Step 3: ユーザーに設計を提示
コードを書く前に、以下の構成で計画をまとめて提示します。作業を進める前に、明示的な承認を得てください。
- アーキテクチャ(アプリ内キャプチャモード、単一ファイル、DEBUG で制御)
- ファイル一覧(作成・変更する正確なパス)
- 画面ごとのキャプチャ計画(各画面への到達方法 — タブのインデックス、ナビゲーションパス、シートの表示操作)
- キャプチャ順の根拠(先にキャプチャする必要がある画面 — 注意点 #5 を参照)
- 要素のレンダリング方法(対象コンポーネントとラップ方法)
- 出力レイアウト(フォルダー構成、命名規則)
- このプロジェクトに関連する既知の注意点(Step 2 で確認したもの)
- 初期化が必要な状態(対象のビューと static var)
Step 4: 実装
templates/ のテンプレートを出発点として使用します。これらは参考パターンであり、そのままコピーして使うひな形ではありません。プロジェクトごとにナビゲーション、モデル、ビューが異なるためです。テンプレートが示すのは基本要素です。それらを対象アプリに合わせて組み立てます。
作成する主なファイル:
<AppName>/Debug/MarketingCapture.swift— キャプチャシステム全体を含むファイル。DEBUG 時のみ使用します。以下を含みます。MarketingCaptureenum(起動引数の解析、出力ヘルパー、ウィンドウのスナップショット、初期化用変数)MarketingCaptureCoordinatorclass([CaptureStep]を順に実行して、それぞれをスナップショットします)MarketingElementHarnessenum(カード、ウィジェット、グラフを ImageRenderer でレンダリングします)
<AppName>/ContentView.swift(またはルートビューのある場所)— データをシードしてコーディネーターを実行する DEBUG 用フック。- 初期化が必要なビュー — DEBUG で制御する
.onAppearフックと.onReceiveの閉じるリスナー。 scripts/capture-marketing.sh— ビルド、インストール、ロケールごとのループ。.gitignore—marketing/を追加。
Step 5: 繰り返し検証する
スクリプトをユーザーに渡して、結果を待つだけにしてはいけません。自分でシミュレーター上で実行し、完了と判断する前に、少なくとも1つのロケールで検証してください。Read ツールで出力された PNG を読み込み、各画面が想定どおりに表示されていることを目視で確認してください。よくある実行時の問題については、以下の「既知の落とし穴」に記載しています。
問題が見つかったら、修正してスクリプト全体を再実行してください(失敗したロケールだけでなく、全体を再実行してください。修正によって、それ以前のロケールで問題が再発することがあります)。その後、目視で再検証してください。
アーキテクチャ:ステップベースのキャプチャ
コーディネーターは、CaptureStep の値のリストを順にたどってキャプチャを進めます。各ステップは自己完結しており、画面への移動方法、待機時間、終了後のクリーンアップ方法を把握しています。
struct CaptureStep {
let name: String // output filename, e.g. "01-home"
let navigate: @MainActor () -> Void // put the app in the right state
let settle: Duration // wait for animations/loads
let cleanup: (@MainActor () -> Void)? // tear down before next step
}
コーディネーターはシンプルなループです。
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)) // cleanup animation
}
異なるナビゲーションパターンに応じたステップの構築
TabView アプリ(最も一般的):
// Simple tab switch — just set the index
CaptureStep(name: "01-home", navigate: { setTab(0) }, settle: .milliseconds(1800), cleanup: nil)
// Tab + presented sheet
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 + ルーターアプリ:
// Push a route onto the stack
CaptureStep(
name: "02-detail",
navigate: { router.push(.itemDetail(item)) },
settle: .milliseconds(1800),
cleanup: { router.popToRoot() }
)
NavigationSplitView アプリ:
// Select sidebar item, then detail
CaptureStep(
name: "03-detail",
navigate: {
sidebarSelection = .recipes
detailSelection = recipes.first
},
settle: .milliseconds(1800),
cleanup: { detailSelection = nil }
)
順序:スタッキングのルール
「クリーンな」ナビゲーション状態が必要な画面は、同じスタックに追加で遷移する画面より先にキャプチャしてください。 子ビュー内のネストされた NavigationPath / @State は、コーディネーターからポップできません。そのため、次のようにします。
Good: Shelf (clean list) → Coffee Detail (pushes onto shelf's stack)
Bad: Coffee Detail → Shelf (stack still has detail pushed)
2つの画面が同じ NavigationStack を共有している場合は、ルートレベルのビューを先にキャプチャしてください。
ビュー状態の初期設定
タイマーのカウントダウン途中、特定の値を表示したグラフ、半分入力済みのフォームなど、デフォルト以外の特定の状態でキャプチャする必要がある画面もあります。手順は次のとおりです。
-
初期設定する値ごとに、
MarketingCaptureにstatic varを追加します。/// Set by the coordinator before presenting the timer view. /// The view reads this in .onAppear to jump to a specific elapsed time. static var pendingElapsedSeconds: Int? /// Set to true to show the assessment overlay on the timer. 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 }
// Items with varied states
let items = [
Item(name: "...", status: .active, ...),
Item(name: "...", status: .lowStock, ...),
// ...enough to fill every screen
]
items.forEach { context.insert($0) }
try? context.save()
}
}
#endif
要素のレンダリング
要素は ImageRenderer を使って 3x スケールでレンダリングし、角丸の外側は透明にします。
カード/リスト行
@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)
guard let image = renderer.uiImage else { continue }
MarketingCapture.writePNG(image, name: "card-\(slugify(item.name))", subfolder: "elements")
}
}
ウィジェット
ウィジェットのビューは、通常 WidgetKit のプロセス内で実行され、システムが提供するパディングや背景に依存しているため、特別な処理が必要です。
@MainActor
static func renderWidget(
name: String,
size: CGSize,
cornerRadius: CGFloat? = nil,
@ViewBuilder content: () -> some View
) {
let isAccessory = size.height <= 80
let radius = cornerRadius ?? (isAccessory ? 8 : 22)
let contentPadding: CGFloat = isAccessory ? 0 : 16
let view = content()
.padding(contentPadding)
.frame(width: size.width, height: size.height)
.background(theme.background)
.clipShape(RoundedRectangle(cornerRadius: radius, style: .continuous))
.environment(\.colorScheme, .light)
let renderer = ImageRenderer(content: view)
renderer.scale = 3
renderer.isOpaque = false
renderer.proposedSize = .init(width: size.width, height: size.height)
guard let image = renderer.uiImage else { return }
MarketingCapture.writePNG(image, name: name, subfolder: "elements")
}
// Standard iPhone widget sizes (points, iPhone 14-17 size class)
enum WidgetSize {
static let small = CGSize(width: 170, height: 170)
static let medium = CGSize(width: 364, height: 170)
static let large = CGSize(width: 364, height: 382)
static let accessoryCircular = CGSize(width: 76, height: 76)
static let accessoryRectangular = CGSize(width: 172, height: 76)
static let accessoryInline = CGSize(width: 257, height: 26)
}
// Usage:
renderWidget(name: "widget-pulse-small", size: WidgetSize.small) {
PulseSmallView(entry: PulseEntry(
date: Date(),
count: 2,
streak: 5,
lastItemName: "Morning Routine"
))
}
グラフ/スタンドアロンビュー
どの SwiftUI ビューも要素としてレンダリングできます。同じ方法でラップします。サイズ、背景、角丸クリップを明示してください。
@MainActor
static func renderChart() {
let chart = MyChartView(values: ChartData.sample)
.frame(width: 420, height: 420)
.background(theme.background)
.clipShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
let renderer = ImageRenderer(content: chart)
renderer.scale = 3
renderer.isOpaque = false
renderer.proposedSize = .init(width: 420, height: 420)
guard let image = renderer.uiImage else { return }
MarketingCapture.writePNG(image, name: "chart-overview", subfolder: "elements")
}
既知の落とし穴
以下は、実際のプロジェクトで発生したバグです。どれも重要な問題として扱ってください。
1. Live Activities はアプリの起動をまたいで残る
ActivityKit Live Activities はプロセスの終了後も残ります。キャプチャ中にアプリが Live Activity を開始した場合(たとえば、タイマーの start() を呼び出すなど)、次のロケールで再起動したときにその Live Activity が引き継がれます。さらに、古い LA が参照しているモデルを新しいシードが削除すると、SwiftData の永続化プロパティに関するアサーションが発生します。
対処法:データに触れる前に、マーケティングキャプチャのブロックの最初で <ActivityManager>.shared.endImmediately() を呼び出してください。また、キャプチャモード中に LA を適切に終了するため、ビューの onDisappear で timerVM.stop()(または同等の処理)も呼び出してください。
2. ロケールごとに再シードしない
ロケールごとに SwiftData + CloudKit をシードすると、同期が頻発し、クラッシュの原因になります。SwiftData ストアは再起動後も保持されます。また、データはロケールに依存しないデモコンテンツなので、初回起動時に一度だけシードし、以降はスキップしてください。
contentVM.fetchItems()
if contentVM.allItems.isEmpty {
DemoDataSeeder.seedIfEmpty(in: modelContext)
contentVM.fetchItems()
}
3. シード前にセットアップする ViewModel は古いスナップショットを保持する
ルートビューの onAppear がマーケティング用のシードを実行する前に someVM.setup(modelContext:) を呼び出すと、VM は空のストアのスナップショットを保持します。シード後に、必要なデータを持つすべての VM に対して someVM.refresh()(または同等の取得メソッド)を呼び出してください。
4. トリガーバインディングを nil にしてもシートは閉じない
親ビューが .fullScreenCover(item: $request) を表示し、request が内部の @State によって制御されている場合、トリガーバインディング(例:pendingItem = nil)を設定してもカバーには何も起きません。カバーは表示されたままなので、次のスクリーンショットには遷移先の画面ではなくカバーが写ります。
修正方法:NotificationCenter を介して閉じる通知を送り、表示中のビューで受信します。
// MarketingCapture.swift
static let dismissSheetNotification = Notification.Name("MarketingCapture.dismissSheet")
// In presented view body
.onReceive(NotificationCenter.default.publisher(for: MarketingCapture.dismissSheetNotification)) { _ in
dismiss()
}
次に、ステップの cleanup で通知を送信し、カバーのアニメーションが完了するまで少なくとも 900ms待ってから次のステップを開始してください。
5. NavigationPath は外部からポップできない
子ビューが @State private var navigationPath = NavigationPath() を保持し、ディープリンクがそこに画面をプッシュする場合、コーディネーターからパスをポップできません。解決策:キャプチャ順序を入れ替え、スタックにプッシュする画面を、クリーンなスタックが必要な画面の後に配置してください。例:まず Shelf をキャプチャし、その後で Coffee Detail に進みます。逆の順序にはしないでください。
6. 通常、ウィジェットビューは拡張機能ターゲット内にのみ存在する
ユーザーのウィジェットビューがウィジェット拡張機能ターゲット内にのみ存在する場合、メインアプリターゲットの MarketingCapture.swift から参照できません。次のいずれかが必要です。
- (a) ウィジェットのビューのファイル(およびそれらのエントリ型と共有ヘルパー)をメインアプリターゲットのメンバーに追加します。プロジェクトで同期フォルダーグループを使用している場合は、
PBXFileSystemSynchronizedBuildFileExceptionSet.membershipExceptionsを編集します。**重大な注意点:membershipExceptionsは除外リストではなく、含めるファイルのリストです。**ここに記載されたファイルは、ターゲットのメンバーであり、除外されません。編集前にこの説明をよく確認してください。 - (b) キャプチャ用ハーネスでのウィジェット描画をスキップし、ユーザーが手動で行うようにします。
また、<App>WidgetBundle.swift は @main を持ち、アプリの @main と競合するため、メインアプリターゲットから除外する必要があります。
7. ImageRenderer + ProgressView(value:total:) = 禁止シンボル
スタイルを明示しない場合、ProgressView の確定値表示は ImageRenderer 経由で合成すると、斜線入りの赤い円として描画されます。修正方法:ProgressView に .progressViewStyle(.linear) を指定します。通常の描画では何も変わらず、描画の不具合を解消します。
8. .containerBackground(for: .widget) はウィジェットコンテキストの外では何もしない
アプリ内で ImageRenderer を使ってウィジェットビューを描画する場合、その .containerBackground は何もしません。ウィジェットの背景は透明になり、コンテンツ外のピクセルはむき出しになります。明示的な背景色と角丸クリップでウィジェットの描画を囲む必要があります。
content()
.padding(16) // widget container normally provides this
.frame(width: size.width, height: size.height)
.background(theme.background)
.clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
iPhone のホーム画面ウィジェットの角丸半径は約 22pt です。ロック画面のアクセサリーの半径は約 8pt です。
9. iOS 26 では iPhone 8 Plus は利用できない
ユーザーが「6.5" iPhone」(App Store の旧サイズ)を指定した場合、iOS 26 以降のシミュレーターには iPhone 8 Plus/iPhone 11 Pro Max が含まれていないことを伝えてください。選択肢:(a) Xcode > Settings > Platforms から古い iOS ランタイムをインストールするか、(b) iOS 26 のデザイン機能を使う場合は、iPhone 17 などの最新の 6.1" モデルに切り替えます。
10. ロケールの起動引数
すべての simctl launch で -AppleLanguages (xx) -AppleLocale xx を渡してください。言語コードを囲む括弧は必須です(plist 配列リテラルです)。フォルダー名には Locale.current.identifier よりも堅牢な Locale.current.language.languageCode?.identifier を使用してください。Locale.current.identifier には en_US のように地域サフィックスが含まれることがあります。
11. ImageRenderer での SwiftUI アニメーション
ImageRenderer は単一のフレームをキャプチャします。アニメーションが終わるまで待つことはありません。コンポーネントに .onAppear アニメーション(グラフの描画や数値のカウントアップなど)がある場合、レンダリングで初期状態がキャプチャされる可能性があります。キャプチャモードではアニメーションを無効にするか、レンダリング前に明示的な待機時間を追加してください。
try? await Task.sleep(for: .milliseconds(500)) // let onAppear animations finish
let renderer = ImageRenderer(content: view)
出力レイアウト
marketing/
<locale>/ e.g. en, de, es, fr, ja
01-home.png
02-<screen>.png
...
NN-<screen>.png
elements/
card-<name>.png
widget-<family>-<size>.png
chart-<name>.png
.gitignore に marketing/ を追加してください。これらはソースではなく、出力ファイルです。
検証チェックリスト
キャプチャパイプラインの完了を宣言する前に、以下を確認してください。
- すべてのロケールで N 個のファイルが生成されている(N = 画面数 + 要素数)
- ロケール間でファイルサイズが異なる(翻訳が実際にレンダリングされたことを確認できます。
en/settings.pngとde/settings.pngのバイト単位の内容が同一なら、ロケールの切り替えが反映されていません) - メインのロケールで画面を 2~3 枚目視し、想定どおりの内容が表示されていることを確認する
- 別のロケールでも同じ画面を確認し、ローカライズされた文字列が含まれていることを確認する
- 少なくとも 1 つのウィジェットのレンダリングと 1 つのカードのレンダリングを確認し、背景と角が適切に表示されていることを検証する
- 別のステップの画面が表示されているスクリーンショットがない(最もよくある不具合は、前のステップのシートが閉じられていないことです)
テンプレート
templates/MarketingCapture.swift.template— ステップベースのコーディネーターを含むキャプチャファイルのひな形です。適用するパターンについては、このスキルの本文を参照してください。templates/capture-marketing.sh.template— シェルスクリプトのひな形です。各プロジェクトに合わせて、バンドル ID、スキーム名、シミュレーター名を置き換えてください。