做 iOS 开发这几年,最让我觉得“明明有更聪明的办法,却偏要人肉硬扛”的环节,就是给 App Store 准备截图。每次版本更新,光是在模拟器里上下滑动找界面、按 Command+S 截屏、再拖进设计工具加标题调尺寸,一套流程走下来,一个下午就没了。更难受的是应用如果支持多语言,同样一套界面,英文截一遍,中文截一遍,日文再截一遍,光点模拟器切语言都点到手酸。所以我第一次看到app-store-screenshots这个项目名的时候,第一反应是“真有救星了”,第二反应是“这东西到底怎么做到自动化的”。
说白了,这个标题对应的核心需求就是:用脚本和配置,把“手工截图+手动加标注+手动生成各尺寸”这套流程,全部变成一条命令的事。它不是什么玄学魔法,也不是图像识别那种高深的玩意儿,而是基于 iOS UI 测试(UITest)的能力,配合 Fastlane 的 snapshot 工具链,让 Xcode 在指定的模拟器、指定的系统版本、指定的语言环境下,自动跑一遍你的 App,每到一个关键页面就按一次“快门”,最后再把这一批原图交给 frameit 统一加边框、加标题、调尺寸,直接输出符合 App Store Connect 上传要求的成套截图。
这篇文章我想把它从头到尾拆开讲。工具叫什么、底层怎么工作的、哪些参数是关键、我在实际跑流程时踩过的坑,都会写清楚。如果你想彻底告别手工截图,或者正在为多语言版本的截图维护发愁,这篇就是给你准备的。
1. 内容整体设计与思路拆解
1.1 自动化截图的核心思路:用 UI 测试当“摄影师”
先说一个最根本的问题:为什么截图能自动化?你不可能用脚本直接去模拟器里“看见”屏幕,再像人一样判断“这个页面加载完了,可以截了”。但 Apple 提供了一个标准方案——UI 测试。Xcode 的 UITest 框架可以用代码控制 App 的每一个界面切换动作,比如点击按钮、滑动列表、输入文字,然后在合适的时机生成“快照”。
screenshot工具(也就是 Fastlane 的 snapshot)做的,就是把这个能力再包装一层。你只需要写一个 UI 测试用例,专门在几个核心页面做停留——进入首页、打开详情、走到支付页——然后在每一个你希望截图的瞬间调用XCUIScreen.main.screenshot(),把图像数据写入临时目录。Fastlane 在背后会自动遍历一批预先定义好的模拟器和语言组合,挨个启动测试、收集截图、最后统一归档。
用一句大白话总结就是:UI 测试用例负责“去哪里拍照”,Fastlane 负责“用哪台相机、调什么语言、最后把照片洗出来”。两者配合,才能做到一条命令生成全套。
1.2 为什么能“一套截图走天下”
很多新手会问一个问题:App Store 要求那么多尺寸,6.7 英寸、6.1 英寸、5.5 英寸、iPad 的 12.9 英寸……我是不是要为每一个尺寸单独截一套图?如果是手工做,确实是这样的噩梦,但app-store-screenshots这套思路不这么干。
它选择的方式是:截好最高分辨率的原图,然后用 frameit 等比缩放并加边框输出不同尺寸。因为 iPhone 的几款屏幕虽然大小不同,但设计稿的逻辑宽度在大多数场景下是一致的,你只需要把一张 6.7 英寸的原图等比缩放到 6.1 英寸的尺寸,然后把多余的空间用边框填上,视觉上几乎是无损的。你真正手工准备的,其实只是“一套主视觉图”。
这背后的逻辑我再展开讲一下。App Store Connect 目前主推 6.7 英寸和 6.1 英寸两种 iPhone 尺寸,如果你在 6.7 英寸模拟器上截出的原始分辨率是 1290 x 2796,那么 6.1 英寸对应的 1179 x 2556 基本就是同比例缩小。frameit 在缩放时会保持画面内容完整,再用设备边框照片把上下区域补齐,从观感上没有任何信息丢失。所以整个方案的核心不是“每种尺寸做一张”,而是“一张原图匹配一个等比缩放框架”,这一个设计思路直接砍掉了 80% 的重复劳动。
1.3 项目定位:不是替代设计,而是替代重复劳动
我得强调一点,app-store-screenshots不是那种“放进去一张图,自动给你生成满分级商店页”的魔改工具。你仍然需要自己决定截哪个页面、加什么卖点文案、用什么颜色的背景边框。它解决的痛点是“执行效率”,不是“创意生成”。
所以这套工具的定位更像你团队里多了一个“不喊累的实习生”:它不会替你想标题,但你告诉它“每个页面加什么标题、用哪台设备、跑几种语言”,它能加班到半夜不抱怨,第二天准时把成套的图交到你手里。理解这个定位很重要,我见过有人期待过高,跑完拿到图说“怎么文案还得我自己写”,这就误解了工具的边界。
2. 核心细节解析与实操要点
2.1 snapshot 的工作原理与关键参数
你不需要了解 Fastlane 全部源码,但几个核心概念必须知道,否则出了问题完全没法排查。
第一个概念是模拟器与语言组合的遍历。Fastlane 的 snapshot 会读取你配置文件里的devices列表和languages列表,然后用嵌套循环的方式挨个组合:第一个模拟器 + 第一种语言跑一遍 UI 测试,收集截图;然后切换到第二种语言再跑一遍;全部跑完,换下一个模拟器。每个组合的截图会按照“设备名/语言代码/序号”的方式存到screenshots目录下。
第二个概念是UI 测试与 Fastlane 的通信机制。很多人好奇:“Fastlane 怎么知道 UI 测试跑到哪一步了?”答案是:你的 UI 测试用例在启动时,会读取 Fastlane 通过环境变量传过去的参数(语言、设备等),并且测试代码会在每个截图点调用一个特殊的帮助方法snapshot(_:)——这个方法内部其实就是XCUIScreen.main.screenshot(),但在此之前它会先执行一段“等待页面稳定”的逻辑,然后保存图片,同时写入一个_fastlane_complete.json标记文件。Fastlane 每截完一张图,就靠这个标记确认“当前这一步完成了,可以继续”。
第三个概念是每次跑完自动生成的 HTML 报告。snapshot 每跑完一轮,会在项目目录下生成一个screenshots.html文件,把所有截图按照设备、语言分门别类展示在一个网页里。这个功能非常实用,你不用一张张打开图片文件,直接在浏览器里滑动就能浏览所有效果。
列一下我常用的一份 snapshot 配置文件核心参数,仅供参考:
snapshot_config = { devices: [ "iPhone 15 Pro Max", "iPhone 15 Pro", "iPad Pro (12.9-inch) (6th generation)" ], languages: ["en-US", "zh-Hans", "ja"], output_directory: "./screenshots", clear_previous_screenshots: true, stop_after_first_error: false, reinstall_app: true, erase_simulator: true, app_identifier: "com.example.myapp", number_of_retries: 2 }几个参数的用途我说一下:clear_previous_screenshots会把旧截图全部删掉,避免混入过期素材;erase_simulator每次跑完抹掉模拟器数据,保证下一次干净启动;number_of_retries在某次测试失败时自动重试,对模拟器的偶发崩溃很有用。
2.2 UI 测试用例的写法:在哪里“按快门”决定了成品质量
配置文件只是告诉 Fastlane 用哪个模拟器跑,真正决定截图内容的,是你写在 UI 测试 target 里的代码。
如果你新建一个 UI Testing Bundle,然后在测试方法里写普通的页面跳转逻辑,最后在关键节点调用snapshot("01-home"),工具就会自动把这一帧保存为01-home.png。通常我会建议把测试方法拆成多个testXxx,每一个对应一组页面场景,这样即使某个场景挂了也不影响其他结果的产出。
这里有个非常重要的实操点:最好给 App 加一个“截图模式”的启动参数。因为正常用户在首次启动时会有引导页、隐私弹窗、网络请求 loading,这些在截图里都是噪音。你可以在 AppDelegate 里判断启动参数里是否包含-snapshot,如果是,就跳过引导页、关闭弹窗、mock 掉网络请求,直接展示核心内容。我在实际项目里就是这么干的,基本保证每张图都是“数据饱满、界面干净”的状态。
用一段伪代码来说明截图测试的结构:
func testCaptureScreenshots() { let app = XCUIApplication() app.launchArguments += ["-snapshot"] app.launch() // 等首页加载完成 let homeTitle = app.staticTexts["home_title"] XCTAssertTrue(homeTitle.waitForExistence(timeout: 5)) snapshot("01-home") // 打开详情页 app.buttons["first_item"].tap() XCTAssertTrue(app.staticTexts["detail_title"].waitForExistence(timeout: 3)) snapshot("02-detail") }注意这里我用的是中文标识符,实际开发中建议直接用英文,主要是为了统一代码风格。
2.3 frameit 的排版能力:给截图穿上“上架衣服”
跑完 snapshot 后,你手上是一堆原始截图,直接用它们上传 App Store Connect 虽然可以过审,但视觉效果一般。这时候轮到 frameit 登场。frameit 可以从一个背景模板、一段标题文案自动合成带有宣传语的“商店风格截图”。
它的配置方式很灵活。你可以在每个截图同目录下放一个Framefile.json,也可以用命令行指定--screenshots_path,然后通过配置文件按设备、语言分别定制。比如英文版的截图想用蓝色背景加“Work Smarter”标题,中文版想用红色背景加“高效工作”标题,都可以在配置里写清楚。
一个典型的 Framefile 内容大致长这样:
{ "default": { "background": "./backgrounds/black.png", "title": { "text": "Capture Everything", "font": "./fonts/Helvetica-Bold.ttf", "color": "#FFFFFF" }, "padding": 80 }, "data": [ { "filter": "01-home", "title": { "text": "Instant Sync" } }, { "filter": "02-detail", "title": { "text": "Powerful Editor" } } ] }filter 字段会匹配文件名包含对应关键字的截图,匹配到的截图会使用这段配置里的文案,其他截图走 default。frameit 默认还会给截图加上设备外框,这样实际显示效果更接近 App Store 官方推荐风格。
2.4 多语言截图自动生成时的三件套配合
多语言版本是大多数开发者的噩梦,但又是app-store-screenshots最擅长解决的场景。你只需要把languages配置项加上目标语言,然后确保Localizable.strings里的文案齐全,snapshot 会在不同语言环境下分别跑一次 UI 测试。截图时自动生成的标题文案也会读取对应语言的 Framefile 配置。
这里有个必须反复验证的细节:截图的文案长度会因语言不同而膨胀。比如同样一句话“分享你的作品”,中文只有三四个词,英文可能变成“Share Your Creation with the World”,如果背景模板固定、字体大小固定,长文案就会被截断或重叠。frameit 提供了text_width_ratio、maximum_text_width这类参数来控制文本缩放,我一般会设置一个最大宽度比例,让长文案自动缩小字号。
但这仍然是个需要人肉检查的环节。我每次跑完框架生成,都会打开screenshots.html把这几种语言翻一遍,重点看有没有文案溢出、字体过小、元素重叠的情况。自动化能帮我把“生成”做到 90 分,但那最后 10 分的视觉效果校对,必须用人的眼睛来把关。
3. 实操过程与核心环节实现
3.1 环境准备:Fastlane、Snapshot、Frameit 的一次性安装
在跑流程之前,你需要在 Mac 上装好 Fastlane。安装方式很简单,macOS 上可以直接用 RubyGems 装,也可以用 Homebrew:
sudo gem install fastlane -NV # 或者 brew install fastlane装完之后,在终端里进入你的 iOS 项目根目录,执行fastlane init初始化。如果项目里已经有 Fastlane 配置,这一步会提示你更新。
接下来需要在 Xcode 里确认 UI Testing Target 已存在,并且按照前面说的方式写了截图测试方法。如果你的测试方法暂时还没写,snapshot 会报错“no tests found”,所以这一步不要跳过。然后你需要在 Fastfile 里定义一个 lane,比如:
lane :screenshots do snapshot( devices: ["iPhone 15 Pro Max"], languages: ["zh-Hans", "en-US"], output_directory: "screenshots" ) frameit( path: "screenshots", path: "screenshots" ) end注意这里两个path参数在不同版本的 Fastlane 里表述不完全一致,新版本里 frameit 直接读取 snapshot 的输出目录。跑完fastlane screenshots,你就应该能看到screenshots目录下按设备/语言分好层的图片文件。
3.2 必要的前置设置:让模拟器每次都是“干净”启动
很多人第一次跑 snapshot 发现截图里会出现上一个测试的残留状态,或者突然弹出一个系统权限对话框,就是因为没有清干净模拟器。我强烈建议配置里开启erase_simulator: true。
这个参数的作用是每次测试前抹掉模拟器的全部数据和设置,让它回到出厂状态。缺点是每次启动 App 会稍微慢一点,多等两三秒,但换来的是绝对的稳定性。同理,reinstall_app: true可以保证每次装的都是最新构建的包,不会出现代码改了截图却没更新的情况。
还有一点要注意:模拟器会有指纹验证弹窗、定位权限弹窗。你的 UI 测试代码里需要提前处理这些系统弹窗。最稳妥的办法是使用addUIInterruptionMonitor来监测并自动点击“允许”按钮。比如定位权限,可以在launch()之后设置一个 monitor:
addUIInterruptionMonitor(withDescription: "Location Permission") { alert in alert.buttons["Allow"].tap() return true }如果你依赖snapshot的自动等待机制,没有处理这类弹窗,测试很可能运行到一半卡住,一直等不到下一个稳定状态。
3.3 参数选择与计算:尺寸、比例与缩放逻辑
App Store Connect 要求不同设备的截图尺寸是精确的,不是你随便 50% 缩放就能通过上传。Fastlane 的 frameit 虽然能自动缩放,但你得确保原图对应的设备型号符合苹果官方的尺寸表。
我直接列几个我常用的尺寸档位,方便你配置模拟器:
| 设备型号 | 截图尺寸(像素) | 对应 App Store 要求 |
|---|---|---|
| iPhone 15 Pro Max | 1290 x 2796 | 6.7 英寸 |
| iPhone 15 Pro | 1206 x 2622 | 6.1 英寸 |
| iPhone 14 Plus | 1290 x 2796 | 6.7 英寸 |
| iPhone SE (3rd) | 750 x 1334 | 5.5 英寸替代 |
| iPad Pro 12.9 | 2048 x 2732 | 12.9 英寸 |
注意一点:App Store Connect 后台现在已经没有单独上传 5.5 英寸的选项,但在部分兼容场景下仍需要 1242 x 2208 的素材。如果用 iPhone SE 截图后直接缩放,会得到一个 750 宽的比例,需要在 frameit 里用背景扩展的方式输出,否则会被认定尺寸不符。我在遇到这类需求时,一般是手动生成一张 1242 x 2208 的背景底图,然后把 750 宽的原图等比居中放入,左右留黑边,这样既满足尺寸要求,也不会拉伸变形。
3.4 实操现场记录:一张检查清单跑完整套
我自己在团队里跑这个流程的时候,会先走一遍检查清单:
- 检查 UI 测试代码是否已在所有目标设备上编译通过
- 检查本地化文案是否完整,尤其新版本新增的界面文案有没有翻译
- 检查 Framefile.json 里的背景图和字体路径是否真实存在
- 执行
fastlane screenshots看跑完是否有失败 - 打开 HTML 报告校对颜色、间距、文案
- 确认所有尺寸的图都在,没有漏设备
这套流程从执行到检查,大概 20 分钟能走完。其中有问题的环节多数不是工具本身,而是 UI 测试代码在同语言情况下点不到某个按钮,或者文案溢出,这些都需要你反复迭代。每迭代一轮,跑一次,比手工截图快太多了。
4. 常见问题与排查技巧实录
4.1 “截图失败:UI 测试找不到元素”
这是出现频率最高的问题。snapshot 因为速度快,有时页面元素还没加载出来,测试代码就已经去点击了。解决办法是两个方向:一是优化测试代码,用waitForExistence(timeout:)增加等待;二是降低帧率,在页面切换后加一点固定的动画等待时间。我一般会在每个截图点前面用 1~2 秒的sleep作为保险,虽然不优雅但稳定。
另外要特别检查是否启用了“慢速动画模式”。Xcode 模拟器有Slow Animations开关,如果打开,所有动画会以慢动作播放,但 UI 测试的点击并不会自动等待动画结束,这会导致你以为页面还在滑动中,其实测试已经在截图了。跑 snapshot 时一定要把慢速动画关闭,否则截出来的图会“糊”在半路。
4.2 截图里的中文字体发虚或显示不全
这种情况多见于 frameit 生成时没有正确指定支持中文的字体。系统自带的 PingFang SC 在模拟器里能渲染,但 frameit 进程直接读取字体文件时可能找不到,导致输出的图片文字变成方块或空白。我的做法是在项目里放一份.ttf或.otf字体文件,然后在 Framefile 里明确指向这个文件,避免依赖系统字体的不确定性。
如果你在编辑截图时发现字体在系统里看着正常,但上传到 App Store 后显示质量下降,那可能是背景模板分辨率不够。frameit 合成时背景图最好大于等于目标尺寸,我一般用 2x 分辨率的背景,避免 upscale 带来的模糊。
4.3 多语言环境下截图文案重叠
长文案是重灾区。即使是日文这种字符宽度不定的语言,稍微长一点就会把布局撑爆。我通常会给不同语言单独设置不同的title字号,而不是全局统一。Framefile 支持针对语言分别配置,你可以在 data 数组里加更多颗粒度的条目。
一个更省事的办法是:截图里的标题不要用完整句子,用两三个词的名词短语。比如“Instant Sync”“Powerful Editor”这种短促有力的词组,基本不会溢出,视觉上也更适合商店页展示。产品功能的细节描述放到 App 描述里说,截图区域留给最核心的记忆点。
4.4 模拟器偶尔卡死或测试中断
模拟器是出了名的吃资源,同时跑三四个设备很容易把 Mac 内存吃满导致测试中断。我建议分批处理:第一天跑 iPhone 系列,第二天跑 iPad 系列,或者先跑一种设备、多种语言,跑完再切设备。把stop_after_first_error设为false,让单个用例失败不至于中断整批任务。
还可以加一层number_of_retries: 2,Fastlane 会在测试崩溃或超时时自动重试当前用例,这能吞掉大部分偶发故障。但注意重试次数不宜超过 3 次,否则故障被掩盖,你反而不知道真实的卡点在哪。
4.5 截图产物目录里出现奇怪的空目录
当你配置了多种设备和语言时,如果某台模拟器上的某一种语言没有截图生成,fastlane 也会留下一个空的目录。这个不一定是错误,可能是你的 App 并没有适配那门语言,导致系统回退到英文。检查一下Localizable.strings是否真的包含了那门语言,如果没有,空目录就是正常现象,不用纠结。
如果目录结构里的文件名出现乱码,多半是模拟器名字里带了空格或中文字符,更新到最新版 Fastlane 或者改掉模拟器名称即可解决。
4.6 上传截图时 App Store Connect 提示“尺寸与要求不符”
这个基本都是误使用了不带边框的原图。frameit 输出的图一般会自动带边框,但你如果清理了backgrounds目录或者模板缺失,frameit 会静默跳过加边框,直接输出原图。这时候上传当然失败。建议在上传前用系统自带的预览工具检查一下所有图片的像素尺寸,或者写一个简单的脚本批量检查:
sips -g pixelWidth -g pixelHeight screenshots/**/*.png提前发现尺寸问题,比你传完再被拒绝省心得多。
4.7 快速排查清单总表
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 测试找不到元素 | 页面未加载完成 | 增加 waitForExistence 或 sleep |
| 截图模糊 | 慢动画开启 | 关闭 Slow Animations |
| 中文显示乱码 | frameit 缺中文字体 | 项目中放 ttf 并指定路径 |
| 文案溢出 | 语言变长 | 分语言设置字号或缩短文案 |
| 模拟器卡死 | 并发设备太多 | 分批执行、降低设备数量 |
| 空目录 | App 不支持该语言 | 检查 Localizable.strings |
| 尺寸报错 | 原图未加边框/背景 | 检查 Framefile 模板 |
5. 从工具到流程:让截图产出融入发布全链路
5.1 把 screenshot lane 嵌入 CI/CD
app-store-screenshots的最终价值不只是“本地跑一下省点时间”,而是能嵌入到团队的持续集成流程里。如果你有 CI 环境,在每次准备发版时自动跑一遍截图,然后把产物存到云端供设计团队检查,这比让开发同学手动跑要稳健得多。
市面上主流的 CI 服务都支持 Fastlane。你只需要在 CI 机器上配置好 Xcode 环境和模拟器运行时,然后在 pipeline 里调用 Fastfile 里定义的 lane 即可。需要注意 CI 机器上的模拟器默认可能是关闭的,需要先用xcrun simctl boot唤醒设备,或者用 Fastlane 的ensure_simulator_devices功能自动创建。
5.2 团队协作中的角色分工
我一直强调截图不是单纯的“开发任务”或者“设计任务”,它天然夹在两者之间。开发负责把 UI 测试写好,让每个核心页面稳定可达;设计负责视觉模板,决定截图用什么背景、什么字体;产品负责文案,决定每个页面想传达什么卖点。三者各管一段,Fastlane 负责打通。
我见过不少团队沟通不畅,开发辛苦跑完截图,设计看完说“标题不应该是这两行字”,产品看完说“文案误导用户”,最后还得重跑一遍。其实只要在刚开始分配好素材和文案,版本更新时按部就班填入,这个流程就不会反复。
5.3 截图模板的版本管理与素材库复用
框架和背景是可以复用的。长期维护一个screenshots-templates目录,下面按产品线和版本号建子目录,保存对应的背景图和 Framefile 配置。新版本更新时,只需要复制上一版的配置,改掉文案和背景色,就能快速产出新的截图。
这样做的好处是产品线之间风格统一,用户打开商店页一看就知道是一家公司的产品。如果你的应用有深色模式,建议准备两套背景模板,一套适配浅色截图,一套适配深色截图,避免在某些页面上文字看不清。
6. 最后再分享一点个人经验
整个工具链跑熟之后,我反而更理解了截图在 App Store 转化里的微妙作用。工具能帮你把效率提升 10 倍,但它没法替你回答“用户在第一眼看到这张图时,会不会想点进去”。截图不仅是设计问题,也是产品表达问题。我通常会在每次跑完截图后,把自己当成一个普通用户,快速滑一眼整套图,看看有没有哪个页面特别无聊,哪个标题完全没吸引力。
根据我的实际经验,iOS 开发者只要愿意花一个下午搭建好这套流程,之后的每一次版本更新都能在十几分钟内拿到全套商店素材。这个投入产出比,我觉得对任何一个团队来说,都是非常划算的事。如果你还没试过,我建议下一次版本迭代就从这个项目开始改造自己的截图流程。