getsentry/SnapshotPreviews

GitHub: getsentry/SnapshotPreviews

该工具自动从 Xcode 预览生成 UI 快照图像并导出,用于视觉回归检测和 Sentry Snapshots 集成,无需编写额外测试代码。

Stars: 551 | Forks: 24

# 📸 SnapshotPreviews [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FEmergeTools%2FSnapshotPreviews%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/EmergeTools/SnapshotPreviews) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FEmergeTools%2FSnapshotPreviews%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/EmergeTools/SnapshotPreviews) 零测试代码即可从你的 Xcode 预览中生成快照图像,并将其导出到磁盘以用于上传至 [Sentry Snapshots](https://docs.sentry.io/product/snapshots/) 或任何其他视觉差异对比服务。支持所有 Apple 平台(iOS / macOS / watchOS / tvOS / visionOS)上使用 `PreviewProvider` 或 `#Preview` 的 SwiftUI 和 UIKit 预览。 ## 安装说明 使用仓库 URL 将该包作为 Swift Package Manager 依赖项添加: ``` https://github.com/EmergeTools/SnapshotPreviews ```

将你的 XCTest target 链接到 `SnapshottingTests` 产品。如果你还想自定义单个预览的渲染(例如精度、布局),可以将 `SnapshotPreferences` 链接到你的 app target。 ## 生成快照 创建一个继承自 `SnapshotTest` 的测试类。无需编写任何测试函数——它们会在运行时自动添加,每个发现的预览对应一个: ``` import SnapshottingTests class DemoAppPreviewTest: SnapshotTest { // Optional: return preview type names like "MyApp.MyView_Previews" to render only a subset. override class func snapshotPreviews() -> [String]? { return nil } // Optional: exclude specific previews from rendering. override class func excludedSnapshotPreviews() -> [String]? { return nil } } ``` 默认情况下,每个渲染的预览都会作为 PNG 附加到 XCTest 结果包中。要在本地将渲染的快照写入磁盘,请运行 `xcodebuild test` 并设置 `TEST_RUNNER_SNAPSHOTS_EXPORT_DIR`,然后检查该目录中生成的 PNG 和 JSON 文件。有关 CI 的使用,请参阅下文的[为 Sentry 导出快照](#exporting-snapshots-for-sentry)。 ![Xcode 测试输出截图](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/76899deaa9184240.png) ### 按模块过滤 如果你的 app 链接了多个框架,你可以将发现范围限定在特定模块: ``` // Only snapshot previews from these modules override class func snapshotPreviewModules() -> [String]? { ["MyFeatureModule"] } // Skip previews from these modules override class func excludedSnapshotPreviewModules() -> [String]? { ["LegacyModule"] } ``` ## 为 Sentry 导出快照 `SnapshotPreviews` 是 [Sentry Snapshots](https://docs.sentry.io/product/snapshots/) 推荐的 iOS 提供源。该流程分为两个步骤:`xcodebuild test` 将 PNG 和 JSON 边车文件写入一个目录,然后 `sentry-cli build snapshots` 上传该目录。 ![Sentry Snapshots 视觉差异对比 UI](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/dccb1ce6af184252.png) ### 1. 从测试运行中导出快照 在测试调用时设置 `TEST_RUNNER_SNAPSHOTS_EXPORT_DIR`。设置后,`SnapshotTest` 会将图像直接写入该目录,而不是将它们附加到 `.xcresult` 包中。 ``` TEST_RUNNER_SNAPSHOTS_EXPORT_DIR="$PWD/snapshot-images" \ xcodebuild test \ -scheme MyApp \ -sdk iphonesimulator \ -destination 'platform=iOS Simulator,name=iPhone 15 Pro' ``` ### 环境变量 SnapshotPreviews 支持以下 test-runner 环境变量: | 变量 | 描述 | | --- | --- | | `TEST_RUNNER_SNAPSHOTS_EXPORT_DIR` | 将渲染的快照 PNG 和 JSON 边车文件写入指定目录,而不是将 PNG 附加到 `.xcresult` 包中。 | | `TEST_RUNNER_SNAPSHOTS_ALL_IMAGE_NAMES_FILE` | 将所有发现的逻辑 `.png` 图像名称写入指定文件,然后直接返回而不渲染预览。用于支持选择性测试工作流。 | 这些模式是互斥的。如果设置了 `TEST_RUNNER_SNAPSHOTS_ALL_IMAGE_NAMES_FILE`,SnapshotPreviews 将仅写入图像名称,不会渲染或导出快照图像。 对于每个渲染的预览,都会写入两个文件: - **`.png`** — 渲染的预览图像。 - **`.json`** — Sentry Snapshots 使用的元数据边车文件。 边车文件包含: | 字段 | 描述 | | --- | --- | | `display_name` | 在 Sentry 中显示的快照名称。根据预览名称、文件路径和模块生成,从而确保导出的文件名保持稳定且明确。 | | `group` | Sentry 用于组织相关快照的分组键。根据预览名称、文件路径和模块生成。 | | `diff_threshold` | 此快照允许的视觉差异。详情请参阅下文。 | | `tags` | 可选的键值对,用于在 Sentry 中过滤和分组快照。 | | `context` | 支持性元数据,例如测试名称、模拟器信息、方向、配色方案、源码行号、预览属性,以及你添加的任何自定义上下文。这些字段会显示在 Sentry UI 的快照详情页面上。 | 当值可用时,SnapshotPreviews 默认会添加这些 `context` 键:`test_name`、`accessibility_enabled`、`simulator.device_name`、`simulator.model_identifier`、`preview.index`、`preview.display_name`、`preview.container_type_name`、`preview.container_display_name`、`preview.preferred_color_scheme`、`preview.orientation` 和 `preview.line`。 使用 `.snapshotAdditionalContext(...)` 可将自定义字段添加到 `context`。自定义上下文会进行浅合并到生成的上下文中,因此像 `"test_name"` 这样的自定义键会替换生成的值。支持的自定义值包括字符串、数字、布尔值和嵌套对象。 使用 `SnapshotPreferences` 产品中的 `.snapshotDiffThreshold(...)` 视图修饰符,可以自定义特定预览允许的视觉差异。例如,`.snapshotDiffThreshold(0.05)` 允许该快照存在高达 5% 的差异。 ``` import SnapshotPreferences #Preview("Map") { MapPreview() .snapshotTags(["screen": "map"]) .snapshotAdditionalContext(["fixture": "city-route"]) .snapshotDiffThreshold(0.05) } ``` 导出器不会写入 Xcode 代码覆盖率数据(`.profraw` / `.profdata`)——仅包含 PNG 和边车文件。如果你需要在同一次测试运行中获取代码覆盖率,请像往常一样在 scheme 中启用它;覆盖率输出将独立进入 `.xcresult` 包。 ### 2. 上传至 Sentry 从以下上传选项中选择一种。 #### 选项 A:`sentry-cli` 使用 [sentry-cli](https://docs.sentry.io/cli/installation/) 3.4.0 或更高版本,并将其指向导出目录: ``` sentry-cli build snapshots "$PWD/snapshot-images" \ --auth-token "$SENTRY_AUTH_TOKEN" \ --app-id com.example.MyApp \ --project my-ios-project ``` 完整的 GitHub Actions 步骤: ``` - name: Run snapshot tests env: TEST_RUNNER_SNAPSHOTS_EXPORT_DIR: ${{ github.workspace }}/snapshot-images run: | xcodebuild test \ -scheme MyApp \ -sdk iphonesimulator \ -destination 'platform=iOS Simulator,name=iPhone 15 Pro' - name: Upload snapshots to Sentry env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} run: | sentry-cli build snapshots "$GITHUB_WORKSPACE/snapshot-images" \ --app-id com.example.MyApp \ --project my-ios-project ``` #### 选项 B:Fastlane 如果你的 CI 已经通过 Fastlane 上传 Apple artifacts,请使用 Sentry 的 Fastlane 集成。有关 Fastlane 配置,请参阅 Sentry 的 [iOS Snapshots 设置文档](https://docs.sentry.io/platforms/apple/guides/ios/snapshots/#step-3-integrate-into-ci)。 有关跨模拟器分片以及 base/head SHA 锁定,请参阅 Sentry 的 [CI 集成文档](https://docs.sentry.io/product/snapshots/integrating-into-ci/)。 ## 提示 ### 唯一的显示名称 为每个预览提供一个唯一的显示名称。这就是在 XCTest 结果以及导出的文件名/元数据中显示的内容: ``` struct MyView_Previews: PreviewProvider { static var previews: some View { MyView().previewDisplayName("My Display Name") } } #Preview("My Display Name") { MyView() } ``` 在使用预览宏时,显示名称应在每个 `PreviewProvider` 内或文件内保持唯一。 ### 快照最佳实践 快照预览应该是确定性的。避免实时网络调用、计时器、无法稳定的动画、依赖于区域设置的数据以及基于当前时钟生成的日期。优先使用固定的 fixtures 和模拟的依赖项,以便在 Xcode、本地测试运行和 CI 中,同一个预览都能渲染出相同的像素。 ### 检测快照环境 在你的单元测试 scheme 中设置 `SNAPSHOTS_RUNNING_FOR_PREVIEWS=1`,以镜像 Xcode 在渲染实时预览时设置的变量。然后,你可以通过一次检查来禁用对预览不友好的行为(日志记录、分析、网络调用): ``` extension ProcessInfo { var isRunningPreviews: Bool { environment["SNAPSHOTS_RUNNING_FOR_PREVIEWS"] == "1" } } ``` ### 快照修饰符 将 `SnapshotPreferences` 产品链接到你的 app target,以便在 `SnapshotTest` 渲染之前自定义各个预览。 | 修饰符 | 使用场景 | 对快照的影响 | | --- | --- | --- | | `.snapshotAccessibility(true)` | 你需要一个专注于无障碍功能的变体。 | 在 iOS 上,通过你的 `SnapshotTest` 配置的无障碍覆盖包装器来渲染快照,显示无障碍元素和标签而不是普通视图。导出的边车文件也会记录已启用无障碍功能。 | | `.snapshotRenderingMode(.coreAnimation)` | 默认渲染器遗漏、出现偏差或错误绘制了视图。 | 更改像素捕获后端。例如,`.coreAnimation` 使用图层树,`.uiView` 使用 UIKit 层级绘制,而 `.window` 捕获整个窗口。不同的模式会影响模糊/材质、地图、动画和其他对渲染器敏感的内容。 | | `.snapshotExpansion(false)` | 你希望保留可见的滚动视口,而不是捕获所有滚动内容。 | 默认情况下,scroll view 会展开,以便快照包含其全部内容。将其设置为 `false` 可使 scroll view 保持在正常的可见高度。 | | `.snapshotTags(["area": "checkout"])` | 你希望在导出的快照上添加可搜索的标签。 | 向 JSON 边车文件添加顶层 `tags`。重复的标签修饰符会合并,后出现的重复键优先。 | | `.snapshotAdditionalContext(["fixture": "loaded"])` | 你需要额外的边车元数据以进行调试或过滤。 | 向 JSON 边车文件的 `context` 对象添加字段。重复的上下文修饰符会合并,后出现的重复键优先。自定义键会覆盖生成的上下文键。 | | `.snapshotDiffThreshold(0.05)` | 快照存在较小的预期像素差异。 | 在导出的边车文件中为此预览设置 `diff_threshold`。`0.05` 允许高达 5% 的像素变化比例,超过此比例 Sentry 才会将图像标记为已更改。 | ### 变体 在多个变体(深色模式、RTL、大文本、无障碍)下渲染同一个视图,可以让你通过单个预览获得更广泛的覆盖。`SnapshotTest` 会渲染预览发出的每一个变体,因此下面的每个 `previewVariant(named:)` 都会成为其自己的快照图像和边车文件。SwiftUI 提供了大多数变体输入(`.dynamicTypeSize(.xxxLarge)`、`.environment(\.layoutDirection, .rightToLeft)` 等)。该包添加了 `.snapshotAccessibility(true)`,它会在快照上覆盖 VoiceOver 元素。 示例应用中的 [`PreviewVariants` 视图](https://github.com/EmergeTools/SnapshotPreviews/blob/main/Examples/DemoApp/DemoApp/TestViews/PreviewVariants.swift) 自动执行 RTL、横屏、无障碍、深色模式和大文本变体: ``` struct MyView_Previews: PreviewProvider { static var previews: some View { PreviewVariants(layout: .sizeThatFits) { MyView(mode: .loaded) .previewVariant(named: "My View - Loaded") MyView(mode: .loading) .previewVariant(named: "My View - Loading") MyView(mode: .error) .previewVariant(named: "My View - Error") } } } ``` ## 附加功能 ### 预览渲染检查(无 PNG) 如果你只想验证每个预览都能在不崩溃的情况下完成布局——例如,为了捕获缺失的 `@EnvironmentObject`——请继承 `PreviewLayoutTest` 而不是 `SnapshotTest`。它运行相同的发现管道,但会跳过图像渲染,因此速度要快得多。这为你提供了*预览覆盖率*(每个预览都被执行了);它不会产生 Xcode 代码覆盖率数据。 ### 预览画廊 `PreviewGallery` 是一个交互式的 SwiftUI 视图,可将你的预览变成可浏览的组件画廊——适用于无法使用 Xcode 的内部构建版本。将你的 app 链接到 `PreviewGallery` 产品,并在任何合理的地方展示它: #### 内部分发构建 `PreviewGallery` 专为开发和受信任的内部分发而设计。如果你想将其包含在 TestFlight 或其他内部构建中,请创建专用的构建配置或归档 scheme,而不是更改你的生产 Release 配置。预览必须编译到该构建中并在优化后保留下来: 1. 将 app target 链接到 `PreviewGallery` 产品。 2. 配置 Swift 优化以便保留预览元数据。Release 配置通常使用全模块优化,即使成功编译,这也可能会移除未被引用的预览声明。对于内部画廊配置,请禁用全模块优化或使用类似 Debug 的编译模式,然后在分发之前验证画廊是否显示了预期的预览。 3. 如果你的预览包含在 `#if DEBUG` 中,请向内部构建配置添加一个自定义 Swift 编译条件,例如 `PREVIEW_GALLERY`。在 Xcode 中,在 **Build Settings > Swift Compiler - Custom Flags > Active Compilation Conditions** (`SWIFT_ACTIVE_COMPILATION_CONDITIONS`) 下进行设置。然后保持预览声明在 Debug 和该内部配置中都能被编译: ``` #if DEBUG || PREVIEW_GALLERY #Preview { MyView() } #endif ``` 对于 `PreviewProvider` 预览,以同样的方式包装提供者类型: ``` #if DEBUG || PREVIEW_GALLERY struct MyView_Previews: PreviewProvider { static var previews: some View { MyView() } } #endif ``` 如果你的预览依赖于 assets、JSON fixtures 或来自 `Preview Content` 文件夹的其他资源,请确保这些资源包含在内部画廊构建中。Xcode Development Assets 专用于预览和 Simulator 开发,且不会增加最终 app 的大小,因此它们可能不存在于你分发的构建中。 仅在受信任的内部构建中公开画廊,并适当地控制访问权限,因为预览可能会泄露未完成的 UI、mock 数据或内部状态。

``` import SwiftUI import PreviewGallery struct InternalSettingsView: View { var body: some View { NavigationStack { Form { Section("Previews") { NavigationLink("Open Gallery") { PreviewGallery() } } } } .navigationTitle("Internal Settings") } } ``` ### 无障碍审计 Xcode [无障碍审计](https://developer.apple.com/documentation/xctest/xcuiapplication/4191487-performaccessibilityaudit) 可以作为 UI 测试的一部分在每个预览上运行。继承 `AccessibilityPreviewTest` 并根据需要覆盖审计类型 / 问题处理程序: ``` import SnapshottingTests import Snapshotting class DemoAppAccessibilityPreviewTest: AccessibilityPreviewTest { override func auditType() -> XCUIAccessibilityAuditType { return .all } override func handle(issue: XCUIAccessibilityAuditIssue) -> Bool { return false } } ``` 有关完整示例,请参阅 `Examples/` 下的演示应用。
它是如何工作的? XCTest 动态插入测试函数的方式是:通过 Objective-C runtime 创建方法并覆盖 XCTest 的 `testInvocations`。 通过解析 `__swift5_proto` Mach-O section 以查找符合 `PreviewProvider`(以及 `#Preview` 生成的相关协议)的类型,在测试二进制文件中发现预览。有关这在 Swift runtime 中如何工作的背景知识:[The Surprising Cost of Protocol Conformances in Swift](https://www.emergetools.com/blog/posts/SwiftProtocolConformance)。
## 二进制框架 虽然 Swift Package Manager 是集成 SnapshotPreviews 的推荐方式,但每个版本也会将框架作为预构建的 XCFramework `.zip` assets 发布: - `SnapshotPreferences.xcframework.zip` — 面向用户的 SwiftUI 偏好设置修饰符。 - `PreviewGallery.xcframework.zip` — 面向用户的预览画廊 UI。 - `SnapshottingTests.xcframework.zip` — 面向用户的 XCTest 快照和布局辅助工具。 - `SnapshotSharedModels.xcframework.zip` — 公共 API 共享的必需支持依赖项。 - `SnapshotPreviewsCore.xcframework.zip` — 用于预览发现和渲染的必需支持依赖项。 - `PreviewsSupport.xcframework.zip` — `SnapshotPreviewsCore` 的必需二进制支持依赖项。 - `Snapshotting.xcframework.zip` — 用于 inserted-dylib 无障碍和 UI 测试工作流的兼容性/runtime artifact。 Xcode 不会推断手动添加的二进制框架之间的传递依赖关系,因此请为每个 target 添加完整的依赖集: - 使用每个预览渲染偏好的 app target:链接并嵌入 `SnapshotPreferences.xcframework` 和 `SnapshotSharedModels.xcframework`。 - 使用画廊 UI 的 app target:链接并嵌入 `PreviewGallery.xcframework`、`SnapshotPreviewsCore.xcframework`、`SnapshotPreferences.xcframework`、`SnapshotSharedModels.xcframework` 和 `PreviewsSupport.xcframework`。 - XCTest 快照/布局 target:链接并嵌入 `SnapshottingTests.xcframework`、`SnapshotPreviewsCore.xcframework`、`SnapshotSharedModels.xcframework` 和 `PreviewsSupport.xcframework`。 - 使用 inserted-dylib 流程的无障碍/UI 测试 target:链接并嵌入 `SnapshottingTests.xcframework`、`Snapshotting.xcframework`、`SnapshotPreviewsCore.xcframework`、`SnapshotSharedModels.xcframework` 和 `PreviewsSupport.xcframework`。 你无需构建任何内容即可使用上述发布的 artifacts。你可以使用仓库根目录下的 `bash build.sh` 从源码在本地构建它们。它需要 Xcode,并会将框架写入 `XCFrameworks/`。 ## 延伸阅读 - [How to use VariadicView, SwiftUI's Private View API](https://www.emergetools.com/blog/posts/how-to-use-variadic-view) — `VariadicView` 是如何为一个 `PreviewProvider` 渲染多个图像的原理。 - [The Surprising Cost of Protocol Conformances in Swift](https://www.emergetools.com/blog/posts/SwiftProtocolConformance) — 如何在 app 二进制文件中发现预览类型。 ## Star 历史 [![Star History Chart](https://api.star-history.com/svg?repos=EmergeTools/SnapshotPreviews&type=Date)](https://star-history.com/#EmergeTools/SnapshotPreviews&Date)
标签:iOS开发, macOS开发, SOC Prime, Swift, UI测试, XCTest, 开发工具, 自动化截图