FastlaneなしでSwiftUIのApp Storeスクリーンショットを自動化する方法(2026年版)
XCUITest + XCUIScreenshot、より少ない定型コードで同じことができる新しい Swift Testing のアタッチメントAPI、そしてシミュレータを起動せずにSwiftUIのビューを直接PNGにレンダリングする ImageRenderer です。swift-snapshot-testing や AppScreenshotKit のようなOSSはこれらをラップしています。ただし、どれもマーケティング用カルーセル(フレーム・キャプション・背景・50言語分のコピー)はコンポーズしてくれません。それは別の工程であり、Mokbiが担うのはまさにそこです。つまり、あなたがキャプチャしたスクリーンショットをコンポーズしてローカライズします。キャプチャ自体は行いません。Fastlaneの snapshot は「App Storeのスクリーンショットを自動化したい」に対する定番の答えであり、それには理由があります — 成熟していてCIフレンドリーだからです。しかしその実態はRubyのツールチェーンであり、Snapfile であり、テストターゲットにコピーする SnapshotHelper.swift であり、その裏側では結局あなたがすでに持っている同じ XCUITest API の薄いラッパーにすぎないものに対して、決して軽くないセットアップが必要になります。SwiftUIでアプリを作っていて、iOSプロジェクトにRubyの依存関係を追加したくないなら、XcodeとSwiftだけでスクリーンショットを自動化できます。この記事は2026年時点のネイティブな選択肢を正直にまとめた地図であり、そのままコピーして使えるコードと、どのツールが何の仕事を担うのかを明確に線引きします。両者を正面から比較したトレードオフについては、Mokbi vs Fastlaneを参照してください。
先にひとつ整理しておくと、多くの混乱が防げます。フレームをキャプチャすること(アプリをある状態まで誘導し、画面に表示されているものをPNGとして保存すること)と、ストアスクリーンショットをコンポーズすること(そのフレームをデバイスモックアップの中に配置し、見出し・背景・翻訳済みコピーを添えて、必要なすべての寸法でエクスポートすること)は、まったく別の問題です。この記事の内容はすべて前者を解決します。後者を解決するものは何ひとつありません。
そもそもなぜFastlaneを避けるのか
Fastlaneに問題があるわけではありません。開発者がネイティブな方法に手を伸ばす理由は、思想的なものではなく実務的なものです。
- Rubyをループに含めなくて済む。Fastlaneはそれ自体のバージョン移り変わり(Bundler、gemの衝突、Rubyのバージョンアップ後にたまに起きるCIの破損)を抱えたRubyのgemです。プロジェクトがそれ以外は純粋なSwiftなら、たった一つの仕事のためにまるごとのランタイムを保守することになります。
- テストターゲットはすでにある。
snapshotは裏側でUIテストを生成します。どのみちUIテストを書いているなら、スクリーンショットのキャプチャは追加2行で済み、新しいツールは不要です。 - SwiftUIが計算を変えた。
ImageRendererを使えば、アプリを起動すらせずに画面をPNGへレンダリングできます。Fastlaneにはこれができません — 常にシミュレータを操作する仕組みだからです。 - CI内の可動部が少なくて済む。スクリーンショットを出力するだけの素の
xcodebuild testは、xcodebuildをシェルアウトしてから結果バンドルを後処理するFastlaneのlaneよりも、動作を追いやすいものです。
その裏返しとして、Fastlaneは面倒な部分(.xcresult バンドルからアタッチメントを回収し、ロケール×デバイスごとに整理し、HTMLプレビューを生成し、deliver でアップロードする)をひとつのコマンドにまとめてくれています。ネイティブな方法を選ぶと、その回収まわりの配管の一部が自分の仕事になります。このトレードオフがすべての判断材料であり、意識的に選ぶ価値があります。
選択肢1:XCUITest + XCUIScreenshot(実際のアプリをキャプチャする)
これはFastlane自体が組み立てられている土台です。UIテストターゲットがシミュレータ内で実際のアプリを起動し、どのUIテストとも同じfinder/tap APIで操作し、XCUIScreen.main がデバイス全体の画面をネイティブ解像度でキャプチャします。そのスクリーンショットを XCTAttachment にラップし、lifetime = .keepAlways を設定することで、テストが成功してもXcodeがそれを保持するようにします(デフォルトでは成功時にアタッチメントを破棄します)。
// 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 バンドルの中に格納されます。実行後に取り出す必要があり、方法はXcodeで結果バンドルを開いてドラッグして取り出すか、CIでは xcparse / xc-screenshot のようなツールでバンドルを走査して名前付きのPNGをフォルダに書き出すかのどちらかです。この抽出工程こそがFastlaneが自動化してくれている部分そのものであり、ネイティブな方法を選ぶことで自分の仕事として引き受ける主なものです。
コード変更なしのローカライズ。ここでの巧みなやり方はXcodeのテストプランです。テストプランをひとつ作り、言語/地域ごとに構成を追加し(それぞれアプリの言語と地域を設定します)、同じテストが構成ごとに1回ずつ実行されます。テストコードには一切手を触れずに、ロケールごとにレンダリングされたアプリUIというローカライズ済みスクリーンショットが手に入ります。App Store Connectが要求する解像度に対応するには、6.9インチのiPhoneクラスのシミュレータと13インチのiPadクラスのシミュレータで実行してください。
メリットとデメリット:
- 実際のアプリのピクセル。実際に動作しているビュー — フォント、テーマ、動的データ、すべてネイティブ — がシミュレータの正確な解像度でレンダリングされます。
- Fastlaneもrubyも不要。純粋なXcode + Swiftです。どのCIでも
xcodebuild testの下で動作します。 - ロケールはコードではなくテストプランの設定。言語ごとに構成を追加して再実行するだけで、Swift側にロケール別の分岐は不要です。
- 抽出は自分で持つ。
.xcresultバンドルからPNGを取り出す作業は自分の仕事になります(一度書けば済むスクリプトですが、実在する手間です)。 - 保守コスト。finder(
buttons["openLibrary"]、タップ)はUIが変わると壊れます。他のUIテストと同じです。
選択肢2:Swift Testingのアタッチメント(モダンで軽量な版)
Swift Testing(Xcode 26に付属する、Appleのマクロベースのフレームワーク)には、XCTAttachment と同じことをより少ない儀式とすっきりした呼び出し方でできる Attachment APIが追加されました。アプリの操作には引き続き XCUIApplication を使い、キャプチャには XCUIScreen.main.screenshot() を使います — これらはXCUITest側にあります — が、画像の記録は @Test 関数の中から Attachment.record(...) で行います。このアタッチメントAPIは画像がどう生成されたかを問わず、条件を満たす画像型であれば何でも受け付けてテストの結果バンドルに書き込みます。
// 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とまったく同じ実際のアプリのピクセルをキャプチャします — 根底にある画面キャプチャの仕組みは同じです。得られるのは、プロジェクトがすでにSwift Testingへ移行済みであれば享受できる、よりすっきりしたSwift Testingの書き味(パラメータ化テスト、デフォルトで async、XCTestCase のサブクラス化が不要)です。避けられないのは同じ抽出工程です。PNGは依然として結果バンドルの中にあり、アップロード用に取り出す必要があります。すでにSwift Testingに乗っているならこちらを使い、そうでなければ選択肢1と結果は同じです。
選択肢3:ImageRenderer(シミュレータをまったく起動しない)
これはFastlaneが構造上どうやっても提供できない選択肢です。ImageRenderer(SwiftUI、iOS 16+ / macOS 13+)はSwiftUIのビューを受け取り、その階層を直接 UIImage / CGImage にレンダリングします — アプリの起動もシミュレータのUI操作もfinderが壊れる心配もありません。App Storeの正確なピクセル寸法にサイズを合わせたビューを渡し、PNGのバイト列を読み戻すだけです。
// 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
}ストア向け出力で重要なことが2つあります。isOpaque = true を設定してください — App Store Connectはアルファチャンネルを持つスクリーンショットを拒否するため、不透明なレンダリングにすればそれを回避できます。そして scale に注意してください。ビューのサイズを対象ピクセルではなくポイント単位で指定する場合は、出力がぼやけないよう renderer.scale をデバイスのスケールに合わせて設定してください。上記のようにフレームを正確な対象ピクセルでサイズ指定している場合は、scaleは1のままにしてください。
大きな注意点は、ImageRenderer がレンダリングできないものです。レンダリングされるのはSwiftUIであり、UIKit/AppKitに支えられたビューではありません。ウェブビュー、マップビュー、メディアプレイヤー、カメラプレビュー、一部のシステムコントロールはプレースホルダーとして出力されます。クリーンで決定的なSwiftUI画面に対しては素晴らしい選択肢です — 瞬時に、ヘッドレスで、寸法もぴったりで、デバイス上を含めどこでも動作します。プラットフォームビューを埋め込んだ画面については、実際のコンポジタ出力をキャプチャするために選択肢1〜2に戻ることになります。
- 圧倒的に高速。画像1枚あたりミリ秒単位で、シミュレータの起動もUI操作も不要です。CIで大活躍します。
- 正確な寸法が手間なく手に入る。フレームを1320 × 2868(または必要な任意のサイズ)にサイズ指定すれば、それがそのまま出力になります — 「App Store向けにリサイズ」という当て推量が不要です。
- 状態は自分で組み立てる。アプリが実行されていないため、モックデータをビューへ直接渡します — 決定的で、デモ用のシード投入用起動引数も不要です。
- SwiftUI専用。プラットフォームに支えられたビューはプレースホルダーとしてレンダリングされます。信頼する前に画面を確認してください。
選択肢4:それらをラップするOSS
隣接する課題を解決する2つのオープンソースプロジェクトが定番として挙がります。
pointfree/swift-snapshot-testing は最も人気のあるSwiftUIスナップショットライブラリであり、その目的を正確に理解しておく価値があります。これは回帰テストのために作られたものであり、ストア用アセットの生成のためではありません。初回実行はリファレンスとなるPNGを記録してわざと失敗し、以降の実行はそのリファレンスとの差分を取り、パディングやフォントの変更で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)))
}
}記録されたリファレンス画像を生のスクリーンショットとして転用することはでき、.image(layout: .device(...)) の戦略は実際のデバイスサイズでレンダリングします — しかしそれはこのツールの厳格さと相性が悪いです(ピクセルパーフェクトな一致を求めるため、マシンごとのアンチエイリアシングの違いが不安定な失敗を引き起こします)。UIの回帰を防ぐために使うべきであり、主要なスクリーンショットパイプラインとしては使わないでください。
AppScreenshotKit(shitamori1272氏による)は、まさにこの仕事のために作られたものです。デバイスクラスとロケールを指定する @AppScreenshot マクロでSwiftUIビューを宣言し、実際の画面をApple純正のデバイスフレームの中でレンダリングするために DeviceView でラップし、Swift Testingの関数からエクスポートします。出力はきれいな Screenshots/<locale>/<device>/ ツリーに整理されます。
// 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/...
}この領域に関連するツールとして知っておく価値があるのは、storescreens のようなCLIです。複数のシミュレータにまたがるXCUITestキャプチャの並列実行、キャプション付きフレーム済みレンダー、Apple APIによるメタデータのアップロードまで、App Store Connectのパイプライン全体をひとつの設定ファイルから動かします。単一で冪等、CIフレンドリーな一連の流れが欲しいなら、これらは本当に役立ちます。ただし、キャプションのレイアウトについても独自の考え方を持ち込んでくるため、そこがマーケティング面において専用のビジュアルエディタが優位に立ちやすい継ぎ目になります。
上記のどれも担わない部分:コンポジションと50言語のローカライズ
ここが境界を明確にした、正直な段落です。上記のどの選択肢も生成するのは素のフレームです — デバイス解像度のアプリUI(あるいは AppScreenshotKit のようなツールでは、素のデバイスモックアップの中に収まったフレーム)です。コンバージョンを生むApp Storeのカルーセルは素のフレームではありません。スタイル付きのデバイスモックアップの中に収まったフレームが、背景の上に、短い見出しとともに、多くの場合マルチパネルのシーケンスとして、狙うすべてのロケールにわたって繰り返されているものです。そのコンポジション工程こそがMokbiの役割です。あなたがキャプチャしたフレーム(XCUITest、ImageRenderer、シミュレータ、どこから来たものでも構いません)をブラウザのエディタに持ち込み、デバイスフレーム・キャプション・背景を追加し、キャプションを最大50のApp Store言語へほぼワンクリックで翻訳し、必要なすべてのサイズで一括エクスポートします。透かし入りプレビューでのデザインは無料で、透かしなしのエクスポートとストア公開はサブスクリプションで無制限に利用できます — Solo は €29.99/mo(1アプリ)、Studio は €49.99/mo(最大5アプリ)で、買い切りの一度きり購入はありません。境界を明確に言い切っておくと、Mokbiはソーススクリーンショットのキャプチャは行いません — あなたのアプリを実行したり、ビューをレンダリングしたりはしません。キャプチャは上記のネイティブSwiftツール側の仕事のままであり、Mokbiはそのツール群が生成したものをコンポーズしてローカライズします。
現実的な組み合わせSwiftUIワークフロー
- ソースフレームをネイティブにキャプチャする。決定的なSwiftUI画面には、正確なピクセルサイズで
ImageRenderer(選択肢3)を使ってレンダリングします — 最速で、シミュレータも不要です。プラットフォームビューを含む画面や、実際のライブ状態が欲しい画面には、XCUITest(選択肢1)またはSwift Testing(選択肢2)のキャプチャを書き、6.9インチのiPhoneクラスと13インチのiPadクラスのシミュレータで実行します。 - 必要ならキャプチャをローカライズする。言語ごとにテストプランの構成を追加してアプリUIを各ロケールでレンダリングするか、
ImageRendererに渡すビューへロケールを渡します。これでローカライズされるのはアプリのピクセルだけであり、マーケティングの見出しではありません。 - マーケティング用カルーセルをコンポーズする。フレームをMokbiに持ち込み、スタイル付きのフレーム・キャプション・背景を追加し、マルチパネルのシーケンスを組み立てます。これはネイティブツールにはできない工程です。
- キャプションを翻訳してエクスポートする。見出しコピーをワンクリックで狙うApp Storeロケールへ一括翻訳し、必要なすべての寸法を一括エクスポートします。
- 次のUI変更時は再キャプチャして開き直す。レンダラーやUIテストを再実行し、保存済みのMokbiプロジェクトを開き直してフレームを差し替え、再エクスポートします。コンポジションと翻訳はそのまま保持されます。
自動化をまるごとスキップすべき場合
年に1〜3回しかリリースしないなら、ネイティブであれFastlaneであれ、どんなスクリーンショットの仕組みを組んでも、それが節約する時間より多くの時間を費やすことになります。iOSシミュレータで正しいデバイスクラスを起動し、各画面を開き、シミュレータのスクリーンショットコマンドを実行すれば(正確なデバイス解像度でキャプチャされます)、10分でApp Store対応のPNGが手に入ります。あとは一度コンポーズしてローカライズし、次に進んでください。上記の自動化がまさに元を取るのは、「手作業で撮り直す」がもはや10分の仕事ではなくなったとき — 頻繁なリリース、多数のロケール、あるいはポートフォリオ内の複数アプリです。見た目の厳密さではなく、リリースの頻度に合わせて仕組みを選んでください。同じ論理は他のプラットフォームにも当てはまります。React Native版のこの問題と、より広い視点のFastlane vs no-codeのトレードオフを参照してください。
何でキャプチャするにせよ、エクスポート前に対象を確認してください。正確なピクセル寸法とフォーマットのルール(PNG/JPEG、RGB、アルファチャンネルなし)はApp Storeスクリーンショットのサイズガイドで確認できます。Appleは1ピクセルのズレも容赦なく却下するため、確認にかける30秒には十分な価値があります。