做 iOS 开发这几年最让我觉得“明明有更聪明的办法却偏要人肉硬扛”的环节就是给 App Store 准备截图。每次版本更新光是在模拟器里上下滑动找界面、按 CommandS 截屏、再拖进设计工具加标题调尺寸一套流程走下来一个下午就没了。更难受的是应用如果支持多语言同样一套界面英文截一遍中文截一遍日文再截一遍光点模拟器切语言都点到手酸。所以我第一次看到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 装也可以用 Homebrewsudo 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()之后设置一个 monitoraddUIInterruptionMonitor(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 Max1290 x 27966.7 英寸iPhone 15 Pro1206 x 26226.1 英寸iPhone 14 Plus1290 x 27966.7 英寸iPhone SE (3rd)750 x 13345.5 英寸替代iPad Pro 12.92048 x 273212.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: 2Fastlane 会在测试崩溃或超时时自动重试当前用例这能吞掉大部分偶发故障。但注意重试次数不宜超过 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/CDapp-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 开发者只要愿意花一个下午搭建好这套流程之后的每一次版本更新都能在十几分钟内拿到全套商店素材。这个投入产出比我觉得对任何一个团队来说都是非常划算的事。如果你还没试过我建议下一次版本迭代就从这个项目开始改造自己的截图流程。