Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Keep replayType as `buffer` for Session Replay triggered by an error (#7804)
- Fix race condition in scope observer notifications causing EXC_BAD_ACCESS during cold launch (#7807)
- Unsubscribe to system event during background to avoid reporting breadcrumbs with wrong timestamps on return to foreground (#7702)
- Fix SwiftUI's images and text redaction in iOS 26 (#7781)

## 9.10.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ final class SentryUIRedactBuilder {
/// Optimized lookup: class IDs with layer constraints (includes both classId and layerId)
private var constrainedRedactClasses: Set<ClassIdentifier> = []

/// Layer class names that should be redacted when they appear as sublayers without a backing UIView.
///
/// Starting with iOS 26 (Liquid Glass), SwiftUI no longer wraps drawing content in UIView subclasses
/// like `SwiftUI.CGDrawingView` or `SwiftUI._UIGraphicsView`. Instead, it renders directly using
/// CALayer sublayers (e.g. `CGDrawingLayer`, `SwiftUI.ImageLayer`, `ColorShapeLayer`) without a
/// backing UIView. This set allows the redaction builder to detect and mask these view-less layers.
private var redactLayerClassIds: Set<String>

/// A set of view type identifier strings that should be excluded from subtree traversal.
///
/// Views matching these patterns will have their subtrees skipped during redaction to avoid crashes
Expand Down Expand Up @@ -141,6 +149,7 @@ final class SentryUIRedactBuilder {
/// `UISlider` and `UISwitch` are ignored by default.
init(options: SentryRedactOptions) { // swiftlint:disable:this function_body_length
var redactClasses = Set<ClassIdentifier>()
var redactLayers = Set<String>()

if options.maskAllText {
redactClasses.insert(ClassIdentifier(objcType: UILabel.self))
Expand Down Expand Up @@ -194,6 +203,8 @@ final class SentryUIRedactBuilder {
redactClasses.insert(ClassIdentifier(classId: "RCTImageView"))
}

Self.registerLiquidGlassLayers(options: options, into: &redactLayers)

#if os(iOS)
redactClasses.insert(ClassIdentifier(objcType: PDFView.self))
redactClasses.insert(ClassIdentifier(objcType: WKWebView.self))
Expand Down Expand Up @@ -238,7 +249,8 @@ final class SentryUIRedactBuilder {
}

redactClassesIdentifiers = redactClasses

redactLayerClassIds = redactLayers

// Compile excluded and included patterns into separate sets for efficient lookup.
// The final decision is computed at runtime using the formula:
//
Expand All @@ -264,6 +276,36 @@ final class SentryUIRedactBuilder {
rebuildOptimizedLookups()
}

/// Registers CALayer class names used by SwiftUI on iOS 26+ (Liquid Glass) for layer-only redaction.
///
/// On iOS 26, SwiftUI no longer wraps drawing content in UIView subclasses. Text, images, and
/// SF Symbols are rendered as CALayer sublayers without a backing UIView. This method populates
/// the layer class set so `mapRedactRegion` can detect and mask them.
private static func registerLiquidGlassLayers(options: SentryRedactOptions, into redactLayers: inout Set<String>) {
guard #available(iOS 26.0, tvOS 26.0, *) else { return }

if options.maskAllText {
// Replaces `SwiftUI.CGDrawingView` for text rendering.
// Base64 of `_TtC7SwiftUIP33_863CCF9D49B535DAEB1C7D61BEE53B5914CGDrawingLayer`
let encodedDrawingLayer = "X1R0QzdTd2lmdFVJUDMzXzg2M0NDRjlENDlCNTM1REFFQjFDN0Q2MUJFRTUzQjU5MTRDR0RyYXdpbmdMYXllcg=="
if let decodedDrawingLayer = encodedDrawingLayer.base64Decoded() {
redactLayers.insert(decodedDrawingLayer)
}
}

if options.maskAllImages {
// Replaces `SwiftUI._UIGraphicsView` + `SwiftUI.ImageLayer` for image rendering.
redactLayers.insert("SwiftUI.ImageLayer")

// Replaces `_UIShapeHitTestingView` for SF Symbol rendering.
// Base64 of `_TtC7SwiftUIP33_E19F490D25D5E0EC8A24903AF958E34115ColorShapeLayer`
let encodedColorShapeLayer = "X1R0QzdTd2lmdFVJUDMzX0UxOUY0OTBEMjVENUUwRUM4QTI0OTAzQUY5NThFMzQxMTVDb2xvclNoYXBlTGF5ZXI="
if let decodedColorShapeLayer = encodedColorShapeLayer.base64Decoded() {
redactLayers.insert(decodedColorShapeLayer)
}
}
}

/// Rebuilds the optimized lookup structures from `redactClassesIdentifiers`.
///
/// This method splits `redactClassesIdentifiers` into two sets for O(1) lookups:
Expand Down Expand Up @@ -399,6 +441,14 @@ final class SentryUIRedactBuilder {
func getRedactClassesIdentifiersTestOnly() -> Set<ClassIdentifier> {
redactClassesIdentifiers
}

func getRedactLayerClassIdsTestOnly() -> Set<String> {
redactLayerClassIds
}

func addRedactLayerClassIdTestOnly(_ classId: String) {
redactLayerClassIds.insert(classId)
}
#endif

/// Identifies and returns the regions within a given `UIView` that need to be redacted.
Expand Down Expand Up @@ -493,6 +543,16 @@ final class SentryUIRedactBuilder {
return true
}

/// Determines whether a CALayer without a backing UIView should be redacted.
///
/// On iOS 26+ (Liquid Glass), SwiftUI renders text, images, and SF Symbols as pure CALayer
/// sublayers without wrapping them in UIView subclasses. This method checks the layer's
/// class name against known SwiftUI drawing layer types.
private func shouldRedactLayer(_ layer: CALayer) -> Bool {
let layerClassId = type(of: layer).description()
return redactLayerClassIds.contains(layerClassId)
}

/// Special handling for `UIImageView` to avoid masking tiny gradient strips and
/// bundle‑provided assets (e.g. SF Symbols or app assets), which are unlikely to contain PII.
private func shouldRedact(imageView: UIImageView) -> Bool {
Expand Down Expand Up @@ -572,6 +632,22 @@ final class SentryUIRedactBuilder {
))
}
}
} else if #available(iOS 26.0, tvOS 26.0, *), !enforceIgnore && shouldRedactLayer(layer) {
// iOS 26+ (Liquid Glass): SwiftUI no longer wraps drawing content in UIView subclasses.
// Text, images, and SF Symbols are rendered as CALayer sublayers without a backing UIView.
// We detect these by matching the layer's class name against known drawing layer types.
//
// We use `.redact` (not `.redactSwiftUI`) so that clip-out regions from
// `sentryReplayUnmask()` can suppress these regions through normal ordering.
// `.redactSwiftUI` is reserved for per-instance SwiftUI view modifier overrides
// which need priority over class-based rules.
redacting.append(SentryRedactRegion(
size: layer.bounds.size,
transform: newTransform,
type: .redact,
name: type(of: layer).description()
))
return
}

// Traverse the sublayers to redact them if necessary
Expand Down
Loading
Loading