不用 Fastlane 自动化 SwiftUI App Store 截图(2026)
XCUITest + XCUIScreenshot 捕获运行中的真实应用,用新的 Swift Testing 附件 API 以更少样板代码做同样的事,以及用 ImageRenderer 直接将 SwiftUI 视图渲染为 PNG,无需运行模拟器。swift-snapshot-testing、AppScreenshotKit 等开源库是对这些能力的封装。它们都无法组合营销轮播图——设备框架、文案、背景、50 种语言的文案本地化。这是单独的一步,也正是 Mokbi 所处的位置:它组合并本地化你已捕获的截图,而不负责捕获截图。对于「如何自动化我的 App Store 截图」这个问题,Fastlane 的 snapshot 是默认答案,而且理由充分——它成熟且对 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 测试,捕获一张截图只是多两行代码——无需引入新工具。 - SwiftUI 改变了算法。 用
ImageRenderer,你可以完全不启动应用就把界面渲染成 PNG,这是 Fastlane 做不到的——它始终要驱动模拟器。 - CI 中的活动部件更少。 一个直接产出截图的
xcodebuild test,比一个调用xcodebuild再对结果包做后处理的 Fastlane lane 更容易理清逻辑。
反过来看:Fastlane 把烦人的部分(从 .xcresult 包中提取附件、按语言 × 设备整理、生成 HTML 预览、通过 deliver 上传)打包进了一条命令。走原生路线,这部分整理工作就变成了你自己的问题。这一取舍就是全部决策所在,值得认真权衡。
方案一:XCUITest + XCUIScreenshot(捕获运行中的真实应用)
这是 Fastlane 本身赖以构建的基础。一个 UI 测试目标在模拟器中启动你的真实应用,用与普通 UI 测试相同的查找/点击 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 的测试计划:创建一个测试计划,为每种语言/地区添加一个配置(各自设置应用语言 + 地区),同一套测试就会按每个配置各跑一次。你会得到本地化的截图——应用界面在每个语言环境下的渲染结果——而无需改动测试代码。在 6.9 英寸 iPhone 机型模拟器和 13 英寸 iPad 机型模拟器上运行,即可覆盖 App Store Connect 要求的分辨率。
优缺点:
- 真实应用像素。 实际运行中的界面——字体、主题、动态数据,全部原生——分辨率精确对应模拟器。
- 无需 Fastlane,无需 Ruby。 纯 Xcode + Swift。可在任何 CI 中通过
xcodebuild test运行。 - 语言是测试计划的配置项,而非代码。 为每种语言添加一个配置再重跑即可;Swift 代码中无需按语言分支。
- 提取工作由你负责。 从
.xcresult包中提取 PNG 需要自己完成(一次性脚本,但确实要写)。 - 维护成本。 查找器(
buttons["openLibrary"]、点击操作)在界面变更时会失效,和任何 UI 测试一样。
方案二:Swift Testing 附件(更现代、更轻量的版本)
Swift Testing(Apple 随 Xcode 26 推出的基于宏的框架)新增了一个 Attachment API,功能与 XCTAttachment 相同,但仪式感更少、调用方式更简洁。你仍然用 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")
}
}从功能上说,这捕获的是与方案一相同的运行中应用像素——底层是同一套屏幕捕获机制。你得到的是更简洁的 Swift Testing 使用体验(参数化测试、默认 async、无需继承 XCTestCase),前提是你的项目已经迁移到了 Swift Testing。你无法绕开的是相同的提取步骤:PNG 依然存放在结果包中,依然需要提取出来才能上传。如果你已经在用 Swift Testing,就选这个方案;否则方案一在结果上完全一致。
方案三:ImageRenderer(完全不运行模拟器)
这是 Fastlane 在结构上根本无法提供的方案。ImageRenderer(SwiftUI,iOS 16+ / macOS 13+)接收一个 SwiftUI 视图,直接把它的视图层级渲染为 UIImage / CGImage——不启动应用,不做模拟器 UI 自动化,没有会失效的查找器。你给它一个尺寸精确对应 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
}对商店输出而言有两件事很关键。设置 isOpaque = true——App Store Connect 会拒绝带 alpha 通道的截图,不透明渲染可以避免这个问题。另外要留意 scale:如果你按点(point)而非目标像素来设置视图尺寸,就要把 renderer.scale 设为设备的缩放比例,否则输出会模糊;如果像上面这样直接按目标像素设置画面尺寸,就把 scale 保持为 1。
最大的注意事项在于 ImageRenderer不能渲染什么。它渲染的是 SwiftUI,而非依托 UIKit/AppKit 的视图:网页视图、地图视图、媒体播放器、相机预览以及部分系统控件都会被渲染成占位符。对于一个干净、确定性的 SwiftUI 界面,它非常出色——即时、无界面、尺寸精确,且可在任何地方运行,包括真机。对于嵌入了平台原生视图的界面,你还是要回到方案一或方案二去捕获真实的合成器输出。
- 速度遥遥领先。 每张图仅需几毫秒,无需模拟器启动,也无需驱动 UI。在 CI 中效果极佳。
- 轻松获得精确尺寸。 把画面尺寸设为 1320 × 2868(或任何所需尺寸),这就是最终输出——无需再猜测「怎么调整成 App Store 要求的尺寸」。
- 你自己构造状态。 由于没有运行中的应用,你可以直接把模拟数据传给视图——结果确定,无需靠启动参数来预置演示数据。
- 仅限 SwiftUI。 依托原生平台的视图会渲染成占位符。使用前请务必核实你的界面。
方案四:对上述方案做封装的开源库
有两个开源项目经常被提及,它们解决的是相邻的问题。
pointfree/swift-snapshot-testing 是最流行的 SwiftUI 快照库,有必要说清楚它的定位:它是为回归测试而生,不是为了产出商店素材。第一次运行会记录一张参考 PNG 并故意失败;之后的运行会与该参考图做差异对比,用来捕捉某天内边距或字体变更悄悄改变了界面。
// 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(...)) 策略也确实会按真实设备尺寸渲染——但这样做会与该工具本身的严格性相悖(它追求像素级精确匹配,不同机器间的抗锯齿差异会导致失败结果不稳定)。用它来防止界面回归就好,不要把它当成你的主要截图流程。
AppScreenshotKit(由 shitamori1272 开发)是专为这个用途打造的。你用 @AppScreenshot 宏声明一个 SwiftUI 视图,指定设备类别和语言,把你真实的界面包进 DeviceView 让它渲染在 Apple 设备样机内,再从一个 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 这样的命令行工具能从单个配置文件驱动整条 App Store Connect 流水线(跨多个模拟器并行的 XCUITest 捕获、带文案的样机渲染、通过 Apple API 上传元数据)。如果你想要一条单一、幂等、对 CI 友好的完整链路,这类工具确实很有用。不过它们对文案排版也自带一套主张——这正是专用的可视化编辑器在营销这一面往往更胜一筹的分界点。
以上所有方案都做不到的部分:组合与 50 种语言本地化
这里说一段有边界、如实的话。上面每个方案产出的都是裸帧——你的应用界面在某个设备分辨率下的样子(用 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按精确像素尺寸渲染(方案三)——这是最快的方式,也不需要模拟器。对于包含平台原生视图的界面,或你想要真实运行状态的场景,写一个 XCUITest(方案一)或 Swift Testing(方案二)捕获,并在 6.9 英寸 iPhone 机型和 13 英寸 iPad 机型模拟器上运行。 - 如需本地化,对捕获过程做本地化处理。 为每种语言添加一个测试计划配置,让应用界面在每个语言环境下渲染,或者把某个语言传给交给
ImageRenderer的视图。这本地化的是应用像素,而非你的营销标题。 - 组合营销轮播图。 把这些帧拖进 Mokbi,加上设计好的设备框架/文案/背景,搭建多面板序列。这是原生工具做不到的一步。
- 翻译文案并导出。 一键把标题文案翻译成你目标 App Store 的各语言版本,再按每个所需尺寸批量导出。
- 下次界面变更时,重新捕获并重新打开。 重跑渲染流程或 UI 测试,重新打开已保存的 Mokbi 项目,替换截图,重新导出。组合方案和翻译结果都会被保留。
什么时候可以完全跳过自动化
如果你一年只发布一到三个版本,搭建任何截图自动化流程——无论原生还是 Fastlane——所花的时间都会超过它能省下的时间。在 iOS 模拟器中以正确的机型运行应用,逐个导航到每个界面,触发模拟器的截图命令(它会以设备的精确分辨率捕获),十分钟内就能得到符合 App Store 要求的 PNG。之后组合并本地化一次,就可以继续别的工作了。上述自动化方案真正划算的时刻,恰恰是「手动重拍」不再是十分钟就能搞定的活儿的时候——频繁发版、语言众多,或者手上有好几款应用要维护。把工具规模匹配你的发版节奏,而不是匹配「看起来专业」的标准。同样的逻辑也适用于其他平台,参见 React Native 版本的同一问题,以及更全面的 fastlane 与无代码工具对比。
无论用哪种方式捕获,导出前都要核实一下目标规格:从 App Store 截图尺寸指南获取精确的像素尺寸和格式规则(PNG/JPEG、RGB、无 alpha 通道)。Apple 对尺寸误差零容忍,花三十秒确认一下值得。