feat(core): Freeze UIActivityIndicatorView spinners during snapshots - #279
Merged
Merged
Conversation
Disabling UIView and SwiftUI animations does not stop UIActivityIndicatorView, which drives its own layer animation. Indeterminate spinners, including the default SwiftUI ProgressView, were captured mid-rotation and produced flaky diffs. Swizzle startAnimating to a no-op from RenderingStrategy.setup so every rendering path skips spinners. Because hidesWhenStopped defaults to true, the indicator is hidden rather than frozen at a fixed frame. This matches the behavior the hosted Emerge runner previously injected outside the library.
Sentry Snapshot Testing
|
Set EMERGE_DISABLE_FREEZE_SPINNERS=1 to keep UIActivityIndicatorView animating, mirroring the existing EMERGE_DISABLE_FIX_TIME opt-out for the clock hook. watchOS ships UIKit without UIActivityIndicatorView, so guard the swizzle the same way other UIKit-only code in this target does. Mark the replacement method dynamic to follow the standard swizzling idiom.
Add a ModuleA preview containing an indeterminate ProgressView so the CI snapshot upload renders a spinner case. With spinner freezing in place the image contains only the label, and any regression shows up as a diff.
jamieQ
reviewed
Sep 15, 2026
Replace company-branded names in the new spinner opt-out and swizzled selector. Keep the Objective-C hook product-specific to avoid collisions, and update the environment-variable test and documentation together.
jamieQ
reviewed
Sep 15, 2026
| 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) |
Member
There was a problem hiding this comment.
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?
Contributor
Author
There was a problem hiding this comment.
cc @noahsmartin
(not going to treat this as a blocker btw)
trevor-e
approved these changes
Sep 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Disabling UIView and SwiftUI animations does not stop
UIActivityIndicatorView, which drives its own layer animation. Indeterminate spinners, including the default SwiftUIProgressView, get captured mid-rotation and produce flaky diffs.This swizzles
startAnimatingto a no-op fromRenderingStrategy.setup(), so every rendering path gets it. SincehidesWhenStoppeddefaults to true, indicators are hidden rather than frozen at a fixed frame, which matches what the hosted runner produced and keeps existing baselines valid. iOS and tvOS only; AppKit has no equivalent view and watchOS has no activity indicator.Opt-out: set
SNAPSHOTS_DISABLE_FREEZE_SPINNERS=1in the test environment to keep spinners animating. Existing baselines that currently show a spinner will change once on upgrade, so this flag is the escape hatch for anyone who needs the old output.Verified end to end with a new
ModuleA Loading Indicatorpreview in MultiModuleDemo, which CI now renders and uploads. Locally, two runs with freezing on produced byte-identical images with no spinner, and two runs with the opt-out set rendered the spinner. Also covered by unit tests for the swizzle and the opt-out, the full package suite on iOS simulator, and builds of all schemes on macOS, Mac Catalyst, iOS, and watchOS.