Fastlane: Make and Ship App Store Screenshots (2026 Guide)
fastlane 是一个用于自动执行 iOS 和 Android 应用发布中繁琐环节的 Ruby 工具链。它的三个与截图相关的操作 —— snapshot、frameit 和 deliver —— 覆盖了完整的流水线:在 XCUITest 中驱动您的应用以捕获原始图像,将其包裹在带有营销文案的设备框架中,并将结果推送到 App Store Connect。本指南将通过您运行它配置所需的文件、车道(lane)定义和 CI 工作流,端到端地梳理该流水线。
在结束时,您将拥有:一个使用 Ruby Bundler 锁定版本的设置、一个 App Store Connect API 密钥、一个正常工作的 Snapfile + SnapshotHelper.swift、一个带有每张截图关键字和标题的 Framefile.json、一个专门针对仅上传截图进行调整的 Deliverfile、一个包含四个车道的 Fastfile,以及一个在 macos-26 运行器上运行整个流程的 GitHub Actions 工作流。
1. 前提条件与思维模型
在安装 any Ruby 之前,先理清概念。截图流水线包含三个独立的阶段,每个阶段由不同的 fastlane 操作负责:
- 捕获 —— XCUITest 在模拟器上运行,并在您希望记录的瞬间调用
snapshot("01-Home")。fastlane 的snapshot操作会启动正确的模拟器,将每个模拟器切换到正确的语言环境(locale),运行测试,并将生成的 PNG 提取到fastlane/screenshots/<locale>/中。 - 框架化 ——
frameit读取每个 PNG,根据图像分辨率选择设备框架,可选地合成背景和营销标题,并在原始图像旁写入一个_framed.png。 - 上传 ——
deliver遍历截图文件夹,将每张图像与 App Store Connect 显示类型(例如 iPhone 6.9"、iPad 13"),并通过 App Store Connect API 替换可编辑 App Store 版本上的截图集。
每个阶段都是独立可运行的。您可以只截取屏幕截图而不上传它们,也可以为其他工具生成的截图添加框架,或者直接上传预先制作好的截图,而完全无需调用模拟器。
2. 推荐的 fastlane 安装方式:Bundler
不要使用 brew install fastlane。建议在项目中使用 Bundler 锁定项目的 fastlane 版本,以便构建项目的每台机器(您的笔记本电脑、同事的笔记本电脑、CI)都运行相同的版本。在项目根目录下执行:
# system Ruby on macOS 14+ is fine, but rbenv/asdf is cleaner
gem install bundler
bundle init
echo 'gem "fastlane"' >> Gemfile
bundle install --path vendor/bundle
# from now on, run fastlane via:
bundle exec fastlane <lane>提交 Gemfile 和 Gemfile.lock。将 vendor/bundle 添加到您的 .gitignore 中。现在,在项目中初始化 fastlane:
bundle exec fastlane init
# choose option 4: "Manual setup"这会创建一个包含 Fastfile 和 Appfile 的 fastlane/ 目录。在 Appfile 中填写您的 bundle 标识符和团队 ID:
# fastlane/Appfile
app_identifier("com.example.myapp")
apple_id("[email protected]") # only needed if you fall back to legacy auth
team_id("ABCDE12345") # Developer Portal team ID3. App Store Connect API 密钥
对于新账户,用户名/密码认证已被弃用,并且对所有人都启用了双重认证保护,这使其在 CI 上无法使用。请改用 App Store Connect API 密钥 。在 App Store Connect → 用户和访问 → 集成 → App Store Connect API 中:
- 生成 API 密钥(每个团队只需执行一次;丢失的
.p8文件无法重新下载)。 - 为其分配 App 管理 角色。开发人员 权限不足以上传截图,而 管理 权限超出了您的实际需求。
- 记录 密钥 ID(10 位字符,例如
ABCD1234EF)以及团队的 签发商 ID(页面顶部的 UUID)。 - 下载
AuthKey_ABCD1234EF.p8文件。
fastlane 会从 JSON 文件中读取密钥。将其保存为 fastlane/asc_api_key.json(并将此路径加入 .gitignore 中):
// fastlane/asc_api_key.json
{
"key_id": "ABCD1234EF",
"issuer_id": "57246542-96fe-1a63-e053-0824d011072a",
"key": "-----BEGIN PRIVATE KEY-----\nMIGTAg...truncated...A==\n-----END PRIVATE KEY-----",
"duration": 1200,
"in_house": false
}duration 是每个生成的 JWT 的有效期(秒);Apple 接受的最大值为 1200(20 分钟)。key 字段是 .p8 文件的完整内容,包括 BEGIN/END PRIVATE KEY 行,换行符使用字面量 \n。
在 CI 上,切勿将该 JSON 文件提交到仓库中。应将 JSON 内容存为单个机密变量(例如 ASC_API_KEY_JSON),并在运行时写入文件 —— 下文的 GitHub Actions 部分有相关示例。
4. snapshot —— 在 XCUITest 中捕获截图
snapshot 通过在您的 UI 测试目标中注入 un 小型 Swift 辅助工具来工作。该辅助工具会挂载到测试运行时,使测试中对 snapshot("name") 的每一次调用都能截取模拟器屏幕、为其命名,并写入 fastlane 已知路径的磁盘位置。
生成辅助工具
bundle exec fastlane snapshot init这会创建 fastlane/Snapfile 和 fastlane/SnapshotHelper.swift。将 SnapshotHelper.swift 添加到 Xcode 中您的 UI 测试目标中(将其拖入,确保 "MyAppUITests" 是唯一勾选的目标 —— 切勿将其包含在应用二进制文件中)。
Snapfile
Snapfile 告知 snapshot 应启动哪些设备、在哪些语言环境下运行测试,以及调用哪个 UI 测试方案:
# fastlane/Snapfile
devices([
"iPhone 17 Pro Max", # 6.9" -> 1320 x 2868 (required slot in 2026)
"iPhone 17 Pro", # 6.3" -> 1206 x 2622 (optional, nicer in store listings)
"iPad Pro 13-inch (M4)", # 13" -> 2064 x 2752 (required if you ship an iPad build)
])
languages([
"en-US",
"de-DE",
"fr-FR",
"es-ES",
"ja",
])
scheme("MyAppUITests") # the UI-testing scheme that runs Snapshot tests
output_directory("./fastlane/screenshots")
clear_previous_screenshots(true)
override_status_bar(true) # 9:41, full battery, full signal
concurrent_simulators(true)
stop_after_first_error(true)
number_of_retries(1)亮点:
devices—— 名称必须与xcrun simctl list devices输出的名称完全匹配。Apple 每年都会更改名称(例如用 "iPhone 17 Pro Max" 代替 "iPhone 16 Pro Max")。如果设备名称错误,snapshot 会默默跳过它。languages—— 传递类似于"en-US"的区域代码或类似于"ja"的基础语言代码。每个条目都会在output_directory下生成一个文件夹。override_status_bar(true)—— 使用simctl status_bar override使每张截图都显示 9:41、满电量和满信号。Apple 技术上并不强制要求这一点,但大多数审核人员都期望看到这样的截图。concurrent_simulators—— 并行运行多个模拟器。这能大幅缩短测试总耗时(大约缩短至设备数的比例),但每个模拟器大约需要消耗 3 GB 内存,因此对资源不足的 CI 运行器来说压力非常大。clear_previous_screenshots—— 在每次运行开始时删除fastlane/screenshots/<locale>/。如果不配置此项,已移除测试用例的旧截图将会永久堆积。
在 XCUITest 中接入 snapshot
SnapshotHelper 暴露了两个全局函数:setupSnapshot(_:)(每个测试调用一次)和 snapshot(_:)(在您希望记录帧的位置调用)。
// MyAppUITests/MyAppUITests.swift
import XCTest
final class MyAppUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
let app = XCUIApplication()
setupSnapshot(app) // injects the locale + screenshot bridge
app.launchArguments += [
"-UITests",
"-AppleLanguages", "(\(Snapshot.deviceLanguage))",
"-AppleLocale", Snapshot.currentLocale,
]
app.launch()
}
func testScreenshots() {
let app = XCUIApplication()
snapshot("01-Home") // tap pattern: drive UI, then snapshot
app.tabBars.buttons["Library"].tap()
snapshot("02-Library")
app.cells.element(boundBy: 0).tap()
snapshot("03-Detail")
app.navigationBars.buttons.element(boundBy: 0).tap()
app.tabBars.buttons["Settings"].tap()
snapshot("04-Settings")
}
}几点实用建议:
- 通过启动参数传入
-UITests并检查该参数,用以在应用中禁用数据分析、替换为确定性的种子数据、跳过引导页或模拟网络请求。审核人员(以及未来的您自己)会为此感谢您的。 snapshot会阻塞直到屏幕截图完成,因此您可以在此之后立即执行下一次交互。- 如果界面有动画,可在启动参数后添加
UIView.setAnimationsEnabled(false)。截屏时如果有动画进行,会导致图片模糊。 - 为截图名称加上数字前缀(
01-,02-),以便文件名顺序与 App Store Connect 的显示顺序匹配。deliver会按字母顺序进行上传。
运行 snapshot
bundle exec fastlane snapshot
# or, equivalently, in a Fastfile lane:
# capture_ios_screenshots输出结果会保存在 fastlane/screenshots/<locale>/iPhone 17 Pro Max-01-Home.png 等路径下。Snapshot 还会生成一个 screenshots.html 文件,您可以直接打开它,在一个页面中快速预览所有语言和设备下的截图。
5. frameit —— 添加设备框架与标题
原始截图只是裸露的设备视口。frameit 会将它们包裹在物理设备框架中,并可选地添加背景、营销标题以及上方更小的“关键字”行。
# Download device frames once (cached under ~/.frameit/)
bundle exec fastlane frameit download_frames
# Frame everything in fastlane/screenshots, including subfolders
bundle exec fastlane frameit --use_platform IOS对于截图目录树中的每一个 foo.png,frameit 都会在其旁边写入一个 foo_framed.png。原始文件会被保留;如果存在框架版本,deliver 会自动读取框架版本(不存在时则回退到原始版本)。
Framefile.json
默认的框架看起来就像开发测试图片 —— 黑色外框、没有背景、没有文本。您可以通过 fastlane/screenshots/Framefile.json 来配置它们:
// fastlane/screenshots/Framefile.json
{
"device_frame_version": "latest",
"default": {
"keyword": {
"font": "./fonts/Inter-SemiBold.ttf",
"color": "#FFFFFF",
"padding": 50
},
"title": {
"font": "./fonts/Inter-Bold.ttf",
"color": "#FFFFFF"
},
"background": "./background.png",
"padding": 80,
"show_complete_frame": false,
"title_below_image": false,
"stack_title": true
},
"data": [
{
"filter": "Home",
"keyword": { "color": "#9F7AEA" },
"frame": "BLACK"
},
{
"filter": "Settings",
"keyword": { "color": "#3B82F6" },
"frame": "WHITE"
}
]
}关键字段:
background—— 被包围的设备所合成到的背景 PNG。尺寸应宽于您输出的最大截图。frameit默认会垂直居中设备;title_below_image和show_complete_frame用于控制文本与外框的交互方式。data[]—— 针对特定截图的覆盖配置,以截图文件名的子字符串作为过滤键。上面的第二条仅适用于名称中带有 “Settings” 的文件。可以用于针对不同的功能行进行不同的色彩搭配。frame—— 覆盖匹配文件的设备框架颜色。frameit在Framefile.json中识别的值包括BLACK、WHITE、GOLD和ROSE_GOLD。框架素材本身会在执行fastlane frameit download_frames时从 Apple 营销资源的 Sketch 文件中下载。
分语言环境的 title.strings 和 keyword.strings
营销文案来自于两个平行的分语言环境 strings 文件 —— 标题使用 title.strings,上方较小的标签使用 keyword.strings。键为不带扩展名或设备前缀的截图文件名:
/* fastlane/screenshots/en-US/title.strings */
"01-Home" = "Track every match.\nIn one tap.";
"02-Library" = "Your full history,\nalways with you.";
"03-Detail" = "Drill into any session.";
"04-Settings" = "Sync across all devices.";/* fastlane/screenshots/en-US/keyword.strings */
/* keywords are rendered above the title, smaller */
"01-Home" = "FAST";
"02-Library" = "ORGANIZED";
"03-Detail" = "DEEP";
"04-Settings" = "EVERYWHERE";翻译文件放入 fastlane/screenshots/de-DE/title.strings 和 fastlane/screenshots/de-DE/keyword.strings、fastlane/screenshots/ja/title.strings 等路径下。换行使用 \n。缺失翻译的语言环境会直接显示未翻译的截图(不渲染标题) —— 这里没有自动退回到英文的机制。
字体
将 TTF 或 OTF 文件放入 fastlane/screenshots/fonts/ 中,并在 Framefile.json 中使用相对路径进行引用。支持可变字体。如果您引用了不存在的字体,frameit 会默默地退回到默认的无衬线字体,而生成的结果可能在没有任何警告的情况下看起来是错的。
6. deliver —— 上传到 App Store Connect
deliver 遍历截图文件夹,将每个 PNG 与 App Store Connect 显示类型进行匹配,然后通过 App Store Connect API 替换当前可编辑版本上的截图集。配置好 API 密钥认证和精简的 Deliverfile 后,上传步骤只需一条命令即可搞定。
目录布局
deliver 需要平铺的分语言环境目录结构 —— 不能包含设备子文件夹。显示类型是根据图像分辨率检测的(在有歧义时,会根据 snapshot 已经加入的文件名设备前缀进行检测):
fastlane/screenshots/
├── en-US/
│ ├── iPhone 17 Pro Max-01-Home_framed.png # 6.9" iPhone
│ ├── iPhone 17 Pro Max-02-Library_framed.png
│ ├── iPad Pro 13-inch (M4)-01-Home_framed.png # 13" iPad
│ ├── title.strings
│ └── keyword.strings
├── de-DE/
│ └── ...
└── Framefile.jsonDeliverfile
# fastlane/Deliverfile
app_identifier("com.example.myapp")
team_id("ABCDE12345")
# Auth via App Store Connect API key (preferred over username/password).
api_key_path("./fastlane/asc_api_key.json")
# Where to read screenshots / metadata from.
screenshots_path("./fastlane/screenshots")
metadata_path("./fastlane/metadata")
# Only push screenshots — leave the binary, pricing, IAPs, etc. alone.
skip_binary_upload(true)
skip_metadata(false)
skip_screenshots(false)
skip_app_version_update(true)
# Don't ask for confirmation in CI.
force(true)
overwrite_screenshots(true)
run_precheck_before_submit(false)
submit_for_review(false)
# Match a screenshot file against the right App Store display family.
# Useful when fastlane's resolution-based detection is ambiguous.
ignore_language_directory_validation(false)最重要的参数:
skip_binary_upload—— 如果不配置此项,deliver会寻找 IPA 包,如果没有则会拒绝运行。skip_metadata—— 如果您只想更新截图且没有fastlane/metadata/目录,可将其设为true。设为false则可以在同一次调用中一并提交本地化的标题、描述、关键字和促销文本。force(true)—— 跳过deliver在推送前显示的确认提示。在 CI 上为必填项。overwrite_screenshots(true)—— 在上传新图前清空每个显示类型中已有的截图。如果不开启,新上传的图会追加在老图后面,直到达到 10 张的限制,然后报错。run_precheck_before_submit(false)——precheck会扫描元数据中是否有风险词汇(“Beta”、竞品名称、敏感词等)。这在提交审核前很有用,但对于仅更新截图的操作来说并不适用。
干跑测试与正式发布
# verify which display families and locales will be touched, no upload
bundle exec fastlane deliver --verify_only
# inspect the resolved configuration
bundle exec fastlane deliver --print_resolved_options
# real upload
bundle exec fastlane deliver在第一次实际上传时,deliver 会在 fastlane/preview.html 中生成一个 HTML 预览文件,并暂停等待确认(除非设置了 force(true))。在引入新设备族时,请务必先打开预览文件查看,因为这是发现显示类型不匹配的最佳时机。
7. 完整的 Fastfile 文件
这一个文件定义了四个车道,涵盖了捕获、框架化、捕获+框架化以及完整发布。本地开发使用一个车道,而 CI 运行则使用另一个车道。
# fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Capture screenshots with snapshot"
lane :screenshots do
capture_ios_screenshots # alias for snapshot
end
desc "Frame screenshots with frameit"
lane :frame do
frame_screenshots(
path: "./fastlane/screenshots",
use_platform: "IOS"
)
end
desc "Build framed screenshots end-to-end"
lane :build_marketing do
capture_ios_screenshots
frame_screenshots(path: "./fastlane/screenshots", use_platform: "IOS")
end
desc "Upload screenshots to App Store Connect"
lane :upload_screenshots do
upload_to_app_store( # alias for deliver
skip_binary_upload: true,
skip_metadata: true,
skip_app_version_update: true,
force: true,
overwrite_screenshots: true,
run_precheck_before_submit: false
)
end
desc "Full release pipeline: capture, frame, upload"
lane :ship_screenshots do
capture_ios_screenshots
frame_screenshots(path: "./fastlane/screenshots", use_platform: "IOS")
upload_to_app_store(
skip_binary_upload: true,
skip_metadata: true,
skip_app_version_update: true,
force: true,
overwrite_screenshots: true
)
end
endcapture_ios_screenshots、frame_screenshots 和 upload_to_app_store 分别是 snapshot、frameit 和 deliver 的规范别名。短名称仍然有效,但长名称更加清晰,且不会遮蔽 Ruby 关键字。
8. 在 GitHub Actions 上配置 CI
截图速度较慢(5 种语言 × 3 款设备通常需要 15 分钟),但可以通过模拟器并行进行。一个运行在 macos-26 上的并行模拟器工作流通常就足够了。
# .github/workflows/screenshots.yml
name: Screenshots
on:
workflow_dispatch:
push:
paths:
- "fastlane/**"
- "MyAppUITests/**"
jobs:
screenshots:
runs-on: macos-26 # Xcode 26.x default; macos-15 also works with explicit xcode-select
timeout-minutes: 90
env:
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
FASTLANE_SKIP_UPDATE_CHECK: "1"
FASTLANE_HIDE_CHANGELOG: "1"
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_26.3.app
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true
- name: Write App Store Connect API key
run: |
mkdir -p fastlane
echo "$ASC_API_KEY_JSON" > fastlane/asc_api_key.json
env:
ASC_API_KEY_JSON: ${{ secrets.ASC_API_KEY_JSON }}
- name: Capture, frame, upload
run: bundle exec fastlane ios ship_screenshots
- name: Archive framed screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: fastlane/screenshots让 CI 运行更可靠的技巧:
- 锁定 Xcode 版本。 使用明确的路径调用
sudo xcode-select -s。GitHub 托管的运行器附带了多个 Xcode 版本,默认版本是他们本月选定的版本。 - 锁定运行器镜像。 使用类似
macos-26或macos-15的带版本号镜像,不要使用macos-latest。在版本发布前三周遇到运行器升级,绝对是您最不想面对的“惊喜”。 - 缓存 Bundler 安装。 在
setup-ruby中使用bundler-cache: true可以让每次运行节省一到两分钟。 - 从单个机密变量中写入 API 密钥。 将
key_id、issuer_id和key存为多个独立的机密变量更方便密钥轮换,但更容易配置出错。对于小团队来说,使用单个 JSON 格式的机密变量就足够了。 - 将框架化的截图上传为工件,即使上传步骤失败也是如此。在重新运行之前,您几乎总是想检查一下究竟生成了什么图。
9. 常见错误及解决方法
"Could not find a device matching..."
Snapshot 中的设备名称必须与 xcrun simctl list devicetypes 完全一致。Apple 每年都会发布最新的旗舰设备,而 Apple 基于年份的版本命名逻辑意味着工具链更新换代非常快。"iPhone 17 Pro Max" 随 Xcode 26(2025 年秋季发布)一同推出;在 Xcode 16 上,6.9 英寸的设备名为 "iPhone 16 Pro Max"。在每次 Xcode 升级后,建议重新运行 xcrun simctl list devicetypes | grep iPhone 并更新您的 Snapfile。
"Unable to verify upload" / 401 Unauthorized
您的 JWT 在上传中途过期、API 密钥被吊销或其角色权限被降级。请检查 asc_api_key.json 中的 duration 是否小于等于 1200,然后重新签发密钥。
"App Store Connect is locked"
您正在上传的版本正处于“正在审核(In Review)”或“等待开发者发布(Pending Developer Release)”状态。在这些状态下,截图是只读的。请创建一个新版本(“准备提交”状态)并重新运行。
截图进入了错误的显示类型
deliver 是根据图像分辨率来匹配显示类型的。如果您导出了一张 1320 × 2868 分辨率的 6.9 英寸 iPhone 截图,它会落入 6.9 英寸的插槽;如果您将其缩放到了 1284 × 2778 分辨率,它会落入旧版的 6.5 英寸插槽。如果您的应用版本中同时存在这两种显示类型,请在文件名中指明设备系列 —— 也就是 snapshot 已经自动生成的文件名前缀。手动导出的截图往往会漏掉这个前缀。
"Screenshot has alpha channel" / 意外的透明度
App Store Connect 历史上一直拒绝带透明通道的 PNG 图像,并且当 deliver 发现 Alpha 通道时仍然会发出警告。虽然 Apple 当前的截图规范页面不再明确声明此规则,但去掉 Alpha 通道是最稳妥的做法。可以使用 sips -s format png 配合 PNG 色彩配置处理该文件,或者重新导出为不带 Alpha 通道的文件。frameit 合成的输出始终是不透明的;但如果您的根视图是不透明的,原始的 simctl io ... screenshot 输出仍然可能会包含 Alpha 通道。
"Locale ru-RU does not exist for this app"
deliver 只会上传到您当前版本中已经创建 of App Store Connect 语言环境。请先在 App Store Connect 后台添加该语言环境(或在 deliver 中设置 force_create_app(true) + 元数据),然后重新运行。
并行模拟器吃光了所有内存
每个在调出键盘状态下启动的 iPhone 17 Pro Max 模拟器都会消耗大约 2.5 GB 内存。在 16 GB 内存的 MacBook 上同时运行三个模拟器会导致频繁使用 Swap 并在测试中途崩溃。建议在本地将 concurrent_simulators(false) 设为 false,将并行测试留给配置更高的 CI 运行器来执行。
10. 何时 fastlane 会显得大材小用
在以下情况下,fastlane 的截图流水线非常好用:
- 您已经拥有一个 UI 测试目标,且团队能够熟练维护 XCUITest 桩数据。
- 您希望将截图作为 CI 构建产物,在每次发布时重新生成。
- 您支持多语言本地化,且希望通过代码一次性定义好版式,而不是针对每种语言单独绘制。
在以下情况下,它不是一个好选择:
- 您想对每张截图进行极致的艺术设计 —— 排版、构图、装饰性元素等。
frameit是一个模板引擎,而非设计工具。 - 您需要发布某项在应用中尚未实现、或仅在测试目标无法触及的 Feature Flag 之后的营销级精美截图。
- 您是一名独立开发者,宁愿在 Mac 或 iPad 应用中设计一次并直接点击 上传。维护 Ruby 工具链、XCUITest 以及锁定 Xcode 版本带来的实际维护成本是相当高的。
对于设计优先的工作流,Screenshot Bro 涵盖了相同的领域 —— 本地化布局、设备框架、使用相同 API 密钥的一键上传 App Store Connect,且无需任何复杂的 XCUITest 配置。
一页纸备忘单
# Setup (once)
bundle init && echo 'gem "fastlane"' >> Gemfile && bundle install
bundle exec fastlane init
bundle exec fastlane snapshot init
bundle exec fastlane frameit download_frames
# Per release
bundle exec fastlane ship_screenshots这就是完整的流水线。安装时执行两个命令,每次发布时执行一个命令。复杂性都被封装在了上述配置文件中,一旦配置好,它们几乎不需要改动。
想知道是完全自己搭建这套系统,还是使用 Mac 桌面端应用来完成同样的工作?您可以查阅 Fastlane snapshot 对比 Screenshot Bro 来了解两者的直接对比和“混合使用”工作流。