Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Examples/MultiModuleDemo/ModuleA/ModuleAViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ public struct ModuleALabel: View {
}
}

public struct ModuleALoadingIndicator: View {
public init() {}

public var body: some View {
VStack(spacing: 12) {
ProgressView()
Text("Loading")
.font(.subheadline)
}
.padding()
}
}

#Preview("ModuleA Button") {
ModuleAButton()
}
Expand All @@ -42,3 +55,7 @@ public struct ModuleALabel: View {
ModuleALabel()
.preferredColorScheme(.dark)
}

#Preview("ModuleA Loading Indicator") {
ModuleALoadingIndicator()
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final class ModuleFilterAssertionTests: XCTestCase {
excludedModules: nil
)

XCTAssertEqual(previews.count, 8)
XCTAssertEqual(previews.count, 9)
XCTAssertTrue(previews.contains { $0.typeName.hasPrefix("ModuleA.") })
XCTAssertTrue(previews.contains { $0.typeName.hasPrefix("ModuleB.") })
XCTAssertTrue(previews.contains { $0.typeName.hasPrefix("ModuleC.") })
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ Display names should be unique within each `PreviewProvider`, or within a file w

Snapshot previews should be deterministic. Avoid live network calls, timers, animations that do not settle, locale-dependent data, and dates generated from the current clock. Prefer fixed fixtures and mocked dependencies so the same preview renders the same pixels in Xcode, local test runs, and CI.

During snapshot rendering, `UIActivityIndicatorView.startAnimating()` is a no-op, so indeterminate spinners (including SwiftUI's default `ProgressView`) never start and are hidden instead of being captured mid-rotation. Set `SNAPSHOTS_DISABLE_FREEZE_SPINNERS=1` in the test environment to opt out.

### Detecting the snapshot environment

Set `SNAPSHOTS_RUNNING_FOR_PREVIEWS=1` in your unit test scheme to mirror the variable Xcode sets when rendering live previews. You can then disable preview-unfriendly behavior (logging, analytics, network calls) with a single check:
Expand Down
27 changes: 27 additions & 0 deletions Sources/SnapshotPreviewsCore/ActivityIndicatorFreezing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#if canImport(UIKit) && !os(watchOS)
import UIKit
import ObjectiveC

enum ActivityIndicatorFreezing {
private static let installed: Void = {
guard
let original = class_getInstanceMethod(UIActivityIndicatorView.self, #selector(UIActivityIndicatorView.startAnimating)),
let replacement = class_getInstanceMethod(UIActivityIndicatorView.self, #selector(UIActivityIndicatorView.snapshotPreviews_frozenStartAnimating))
else { return }
method_exchangeImplementations(original, replacement)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you all tried this in the past, but could/should we just disable core animation 'actions' around the point where the snapshots are rendered? Or does that not work for some reason?

@NicoHinderling NicoHinderling Sep 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @noahsmartin

(not going to treat this as a blocker btw)

}()

static func isEnabled(environment: [String: String] = ProcessInfo.processInfo.environment) -> Bool {
environment["SNAPSHOTS_DISABLE_FREEZE_SPINNERS"] != "1"
}
Comment thread
NicoHinderling marked this conversation as resolved.

static func install() {
guard isEnabled() else { return }
_ = installed
}
}

private extension UIActivityIndicatorView {
@objc dynamic func snapshotPreviews_frozenStartAnimating() {}
}
Comment thread
sentry[bot] marked this conversation as resolved.
#endif
3 changes: 3 additions & 0 deletions Sources/SnapshotPreviewsCore/RenderingStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ private let testHandler: NSObject.Type? = NSClassFromString("EMGTestHandler") as

extension RenderingStrategy {
static func setup() {
#if canImport(UIKit) && !os(watchOS)
ActivityIndicatorFreezing.install()
#endif
testHandler?.perform(NSSelectorFromString("setup"))
}
}
25 changes: 25 additions & 0 deletions Tests/SnapshotPreviewsTests/ActivityIndicatorFreezingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#if canImport(UIKit) && !os(watchOS)
import XCTest
import UIKit
@testable import SnapshotPreviewsCore

final class ActivityIndicatorFreezingTests: XCTestCase {
@MainActor
func testStartAnimatingIsNoOpAfterRenderingSetup() {
UIKitRenderingStrategy.setup()

let indicator = UIActivityIndicatorView(style: .medium)
indicator.startAnimating()

XCTAssertFalse(indicator.isAnimating)
}

func testFreezingIsEnabledByDefault() {
XCTAssertTrue(ActivityIndicatorFreezing.isEnabled(environment: [:]))
}

func testFreezingIsDisabledByEnvironmentVariable() {
XCTAssertFalse(ActivityIndicatorFreezing.isEnabled(environment: ["SNAPSHOTS_DISABLE_FREEZE_SPINNERS": "1"]))
}
}
#endif
Loading