From addbc1988d756751c0aa14f33e398732bc71e0f6 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Tue, 7 Apr 2026 17:22:37 -0300 Subject: [PATCH 1/4] feat: enhance SentryUIRedactBuilder to support redaction of CALayer sublayers in iOS 26+ --- .../ViewCapture/SentryUIRedactBuilder.swift | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift b/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift index e208b713ba..019f50843e 100644 --- a/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift +++ b/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift @@ -106,6 +106,14 @@ final class SentryUIRedactBuilder { /// Optimized lookup: class IDs with layer constraints (includes both classId and layerId) private var constrainedRedactClasses: Set = [] + /// 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 + /// 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 @@ -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() + var redactLayers = Set() if options.maskAllText { redactClasses.insert(ClassIdentifier(objcType: UILabel.self)) @@ -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)) @@ -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: // @@ -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) { + 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: @@ -399,6 +441,10 @@ final class SentryUIRedactBuilder { func getRedactClassesIdentifiersTestOnly() -> Set { redactClassesIdentifiers } + + func getRedactLayerClassIdsTestOnly() -> Set { + redactLayerClassIds + } #endif /// Identifies and returns the regions within a given `UIView` that need to be redacted. @@ -493,6 +539,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 { @@ -572,6 +628,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 From 184410781dcb0e1eec98b5eb58dba75cbb089fd0 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Tue, 7 Apr 2026 18:48:35 -0300 Subject: [PATCH 2/4] Add tests for layer redacting + swiftui views --- .../ViewCapture/SentryUIRedactBuilder.swift | 4 + .../SentryUIRedactBuilderTests+SwiftUI.swift | 329 ++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift diff --git a/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift b/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift index 019f50843e..1c25678fe3 100644 --- a/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift +++ b/Sources/Swift/Core/Tools/ViewCapture/SentryUIRedactBuilder.swift @@ -445,6 +445,10 @@ final class SentryUIRedactBuilder { func getRedactLayerClassIdsTestOnly() -> Set { redactLayerClassIds } + + func addRedactLayerClassIdTestOnly(_ classId: String) { + redactLayerClassIds.insert(classId) + } #endif /// Identifies and returns the regions within a given `UIView` that need to be redacted. diff --git a/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift b/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift new file mode 100644 index 0000000000..137facc14c --- /dev/null +++ b/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift @@ -0,0 +1,329 @@ +// swiftlint:disable file_length + +#if os(iOS) && !targetEnvironment(macCatalyst) +@_spi(Private) @testable import Sentry +import SwiftUI +import UIKit +import XCTest + +/// Dummy CALayer subclass used to simulate iOS 26 layer-only SwiftUI rendering +/// in unit tests. The builder matches layers by `type(of:).description()`, so +/// we inject this class's name via `addRedactLayerClassIdTestOnly`. +private class DummyRedactableLayer: CALayer {} + +// MARK: - Layer-Only Redaction Unit Tests + +/// Tests for iOS 26+ (Liquid Glass) layer-only redaction, where SwiftUI renders +/// content as CALayer sublayers without backing UIViews. +class SentryLayerRedactionTests: SentryUIRedactBuilderTests { + + // MARK: - Helpers + + private func getSut(maskAllText: Bool = true, maskAllImages: Bool = true) -> SentryUIRedactBuilder { + return SentryUIRedactBuilder(options: TestRedactOptions( + maskAllText: maskAllText, + maskAllImages: maskAllImages + )) + } + + /// Creates a root UIView with a DummyRedactableLayer sublayer (no backing UIView), + /// and a builder configured to redact that layer class. + private func makeSutAndRootView( + maskAllText: Bool = true, + maskAllImages: Bool = true, + sublayerFrame: CGRect = CGRect(x: 10, y: 10, width: 60, height: 20), + rootFrame: CGRect = CGRect(x: 0, y: 0, width: 100, height: 100) + ) -> (sut: SentryUIRedactBuilder, rootView: UIView) { + let sut = getSut(maskAllText: maskAllText, maskAllImages: maskAllImages) + sut.addRedactLayerClassIdTestOnly(DummyRedactableLayer.description()) + + let rootView = UIView(frame: rootFrame) + let sublayer = DummyRedactableLayer() + sublayer.frame = sublayerFrame + rootView.layer.addSublayer(sublayer) + + return (sut, rootView) + } + + // MARK: - Initialization: base64-encoded layer classes decoded correctly + + func testBase64EncodedLayerClassesAreCorrectlyDecoded() throws { + guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let sut = getSut(maskAllText: true, maskAllImages: true) + let layerIds = sut.getRedactLayerClassIdsTestOnly() + + XCTAssertTrue( + layerIds.contains("_TtC7SwiftUIP33_863CCF9D49B535DAEB1C7D61BEE53B5914CGDrawingLayer"), + "CGDrawingLayer should be registered for text masking" + ) + XCTAssertTrue( + layerIds.contains("SwiftUI.ImageLayer"), + "SwiftUI.ImageLayer should be registered for image masking" + ) + XCTAssertTrue( + layerIds.contains("_TtC7SwiftUIP33_E19F490D25D5E0EC8A24903AF958E34115ColorShapeLayer"), + "ColorShapeLayer should be registered for SF Symbol masking" + ) + } + + func testLayerClassesNotRegistered_whenMaskAllTextDisabled() throws { + guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let sut = getSut(maskAllText: false, maskAllImages: true) + let layerIds = sut.getRedactLayerClassIdsTestOnly() + + XCTAssertFalse( + layerIds.contains("_TtC7SwiftUIP33_863CCF9D49B535DAEB1C7D61BEE53B5914CGDrawingLayer"), + "CGDrawingLayer should not be registered when maskAllText is false" + ) + XCTAssertTrue(layerIds.contains("SwiftUI.ImageLayer")) + } + + func testLayerClassesNotRegistered_whenMaskAllImagesDisabled() throws { + guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let sut = getSut(maskAllText: true, maskAllImages: false) + let layerIds = sut.getRedactLayerClassIdsTestOnly() + + XCTAssertFalse( + layerIds.contains("SwiftUI.ImageLayer"), + "SwiftUI.ImageLayer should not be registered when maskAllImages is false" + ) + XCTAssertFalse( + layerIds.contains("_TtC7SwiftUIP33_E19F490D25D5E0EC8A24903AF958E34115ColorShapeLayer"), + "ColorShapeLayer should not be registered when maskAllImages is false" + ) + XCTAssertTrue(layerIds.contains("_TtC7SwiftUIP33_863CCF9D49B535DAEB1C7D61BEE53B5914CGDrawingLayer")) + } + + func testNoLayerClassesRegistered_whenBothMaskingDisabled() throws { + guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let sut = getSut(maskAllText: false, maskAllImages: false) + let layerIds = sut.getRedactLayerClassIdsTestOnly() + + XCTAssertTrue(layerIds.isEmpty, "No layer classes should be registered when all masking is disabled") + } + + // MARK: - Layer-only redaction via mapRedactRegion + + func testRedact_layerOnlySublayer_shouldRedact() throws { + let (sut, rootView) = makeSutAndRootView(sublayerFrame: CGRect(x: 10, y: 10, width: 80, height: 20)) + let result = sut.redactRegionsFor(view: rootView) + + let redactRegions = result.filter { $0.type == .redact } + XCTAssertEqual(redactRegions.count, 1) + XCTAssertEqual(redactRegions.first?.size, CGSize(width: 80, height: 20)) + } + + func testRedact_layerOnlySublayer_notRedacted_whenClassNotRegistered() { + let sut = getSut() + // Don't call addRedactLayerClassIdTestOnly — DummyRedactableLayer is not registered + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let sublayer = DummyRedactableLayer() + sublayer.frame = CGRect(x: 10, y: 10, width: 60, height: 20) + rootView.layer.addSublayer(sublayer) + + let result = sut.redactRegionsFor(view: rootView) + + let redactRegions = result.filter { $0.type == .redact } + XCTAssertEqual(redactRegions.count, 0, "Unregistered layer class should not be redacted") + } + + // MARK: - Hidden / transparent layer-only sublayers + + func testRedact_hiddenLayerOnlySublayer_shouldNotRedact() { + let (sut, rootView) = makeSutAndRootView() + rootView.layer.sublayers?.last?.isHidden = true + + let result = sut.redactRegionsFor(view: rootView) + + let redactRegions = result.filter { $0.type == .redact } + XCTAssertEqual(redactRegions.count, 0, "Hidden layer-only sublayer should not be redacted") + } + + func testRedact_zeroOpacityLayerOnlySublayer_shouldNotRedact() { + let (sut, rootView) = makeSutAndRootView() + rootView.layer.sublayers?.last?.opacity = 0 + + let result = sut.redactRegionsFor(view: rootView) + + let redactRegions = result.filter { $0.type == .redact } + XCTAssertEqual(redactRegions.count, 0, "Fully transparent layer-only sublayer should not be redacted") + } + + // MARK: - Unknown layer types should not be redacted + + func testRedact_unknownLayerOnlySublayer_shouldNotRedact() { + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let sublayer = CALayer() + sublayer.frame = CGRect(x: 10, y: 10, width: 60, height: 20) + rootView.layer.addSublayer(sublayer) + + let sut = getSut() + let result = sut.redactRegionsFor(view: rootView) + + let redactRegions = result.filter { $0.type == .redact } + XCTAssertEqual(redactRegions.count, 0, "Unknown CALayer sublayer should not be redacted") + } + + // MARK: - Region type is .redact (not .redactSwiftUI) + + func testRedact_layerOnlySublayer_producesRedactType_notRedactSwiftUI() { + let (sut, rootView) = makeSutAndRootView() + let result = sut.redactRegionsFor(view: rootView) + + let swiftUIRegions = result.filter { $0.type == .redactSwiftUI } + let redactRegions = result.filter { $0.type == .redact } + + XCTAssertEqual(swiftUIRegions.count, 0, "Layer-only sublayers should not produce .redactSwiftUI regions") + XCTAssertEqual(redactRegions.count, 1, "Layer-only sublayers should produce .redact regions") + } + + // MARK: - enforceIgnore respected + + func testRedact_layerOnlySublayer_notRedacted_whenParentUnmasked() { + let sut = getSut() + sut.addRedactLayerClassIdTestOnly(DummyRedactableLayer.description()) + + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + let container = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + container.sentryReplayUnmask() + rootView.addSubview(container) + + let sublayer = DummyRedactableLayer() + sublayer.frame = CGRect(x: 10, y: 10, width: 60, height: 20) + container.layer.addSublayer(sublayer) + + let result = sut.redactRegionsFor(view: rootView) + + let redactRegions = result.filter { $0.type == .redact } + XCTAssertEqual(redactRegions.count, 0, "Layer-only sublayer should not be redacted when parent is unmasked") + } +} + +// MARK: - SwiftUI Integration Tests + +/// End-to-end tests verifying SwiftUI views are properly masked using real +/// UIHostingController rendering. These tests work across all iOS versions. +class SentrySwiftUIRedactionIntegrationTests: SentryUIRedactBuilderTests { + + // MARK: - Text + + func testSwiftUITextIsMasked() { + let window = hostSwiftUIViewInWindow( + VStack { Text("Hello SwiftUI").font(.system(size: 20)).padding(20) }, + frame: CGRect(x: 0, y: 0, width: 300, height: 300) + ) + + let sut = SentryUIRedactBuilder(options: SentryRedactDefaultOptions()) + let result = sut.redactRegionsFor(view: window.rootViewController!.view!) + + let redactRegions = result.filter { $0.type == .redact || $0.type == .redactSwiftUI } + XCTAssertGreaterThanOrEqual(redactRegions.count, 1, "SwiftUI.Text should be masked") + } + + func testSwiftUITextNotMaskedWhenTextMaskingDisabled() { + let window = hostSwiftUIViewInWindow( + VStack { Text("Hello SwiftUI").font(.system(size: 20)).padding(20) }, + frame: CGRect(x: 0, y: 0, width: 300, height: 300) + ) + + let options = SentryRedactDefaultOptions() + options.maskAllText = false + let sut = SentryUIRedactBuilder(options: options) + let result = sut.redactRegionsFor(view: window.rootViewController!.view!) + + let redactRegions = result.filter { $0.type == .redact || $0.type == .redactSwiftUI } + XCTAssertEqual(redactRegions.count, 0, "SwiftUI.Text should not be masked when maskAllText is disabled") + } + + // MARK: - Image + + func testSwiftUIImageIsMasked() { + let image = UIGraphicsImageRenderer(size: CGSize(width: 40, height: 40)).image { context in + UIColor.green.setFill() + context.fill(CGRect(x: 0, y: 0, width: 40, height: 40)) + } + + let window = hostSwiftUIViewInWindow( + VStack { Image(uiImage: image) }, + frame: CGRect(x: 0, y: 0, width: 300, height: 300) + ) + + let sut = SentryUIRedactBuilder(options: SentryRedactDefaultOptions()) + let result = sut.redactRegionsFor(view: window.rootViewController!.view!) + + let redactRegions = result.filter { $0.type == .redact || $0.type == .redactSwiftUI } + XCTAssertGreaterThanOrEqual(redactRegions.count, 1, "SwiftUI.Image should be masked") + } + + func testSwiftUIImageNotMaskedWhenImageMaskingDisabled() { + let image = UIGraphicsImageRenderer(size: CGSize(width: 40, height: 40)).image { context in + UIColor.green.setFill() + context.fill(CGRect(x: 0, y: 0, width: 40, height: 40)) + } + + let window = hostSwiftUIViewInWindow( + VStack { Image(uiImage: image) }, + frame: CGRect(x: 0, y: 0, width: 300, height: 300) + ) + + let options = SentryRedactDefaultOptions() + options.maskAllImages = false + let sut = SentryUIRedactBuilder(options: options) + let result = sut.redactRegionsFor(view: window.rootViewController!.view!) + + let redactRegions = result.filter { $0.type == .redact || $0.type == .redactSwiftUI } + XCTAssertEqual(redactRegions.count, 0, "SwiftUI.Image should not be masked when maskAllImages is disabled") + } + + // MARK: - SF Symbol + + func testSwiftUISFSymbolIsMasked() { + let window = hostSwiftUIViewInWindow( + VStack { Image(systemName: "star.fill").font(.system(size: 40)) }, + frame: CGRect(x: 0, y: 0, width: 300, height: 300) + ) + + let sut = SentryUIRedactBuilder(options: SentryRedactDefaultOptions()) + let result = sut.redactRegionsFor(view: window.rootViewController!.view!) + + let redactRegions = result.filter { $0.type == .redact || $0.type == .redactSwiftUI } + XCTAssertGreaterThanOrEqual(redactRegions.count, 1, "SwiftUI.Image(systemName:) should be masked") + } + + // MARK: - Label + + func testSwiftUILabelIsMasked() { + let window = hostSwiftUIViewInWindow( + VStack { Label("Hello SwiftUI", systemImage: "house").labelStyle(.titleAndIcon) }, + frame: CGRect(x: 0, y: 0, width: 300, height: 300) + ) + + let sut = SentryUIRedactBuilder(options: SentryRedactDefaultOptions()) + let result = sut.redactRegionsFor(view: window.rootViewController!.view!) + + let redactRegions = result.filter { $0.type == .redact || $0.type == .redactSwiftUI } + XCTAssertGreaterThanOrEqual(redactRegions.count, 2, "SwiftUI.Label should mask both text and image") + } + + // MARK: - List + + func testSwiftUIListTextIsMasked() { + let window = hostSwiftUIViewInWindow( + VStack { + List { + Section("Section 1") { Text("Item 1") } + Section { Text("Item 2") } + } + }, + frame: CGRect(x: 0, y: 0, width: 300, height: 500) + ) + + let sut = SentryUIRedactBuilder(options: SentryRedactDefaultOptions()) + let result = sut.redactRegionsFor(view: window.rootViewController!.view!) + + let redactRegions = result.filter { $0.type == .redact || $0.type == .redactSwiftUI } + XCTAssertGreaterThanOrEqual(redactRegions.count, 1, "SwiftUI.List text items should be masked") + } +} +#endif +// swiftlint:enable file_length From 1fe3374e599ba47e288aaa20ec068450d3d626f6 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Mon, 20 Apr 2026 16:59:39 -0300 Subject: [PATCH 3/4] Skip layer tests on version lower than iOS 26 --- .../SentryUIRedactBuilderTests+SwiftUI.swift | 30 +++++++++++++++---- scripts/.clang-format-version | 2 +- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift b/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift index 137facc14c..eae6dfa7ae 100644 --- a/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift +++ b/Tests/SentryTests/ViewCapture/SentryUIRedactBuilderTests+SwiftUI.swift @@ -49,6 +49,7 @@ class SentryLayerRedactionTests: SentryUIRedactBuilderTests { func testBase64EncodedLayerClassesAreCorrectlyDecoded() throws { guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let sut = getSut(maskAllText: true, maskAllImages: true) let layerIds = sut.getRedactLayerClassIdsTestOnly() @@ -68,6 +69,7 @@ class SentryLayerRedactionTests: SentryUIRedactBuilderTests { func testLayerClassesNotRegistered_whenMaskAllTextDisabled() throws { guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let sut = getSut(maskAllText: false, maskAllImages: true) let layerIds = sut.getRedactLayerClassIdsTestOnly() @@ -80,6 +82,7 @@ class SentryLayerRedactionTests: SentryUIRedactBuilderTests { func testLayerClassesNotRegistered_whenMaskAllImagesDisabled() throws { guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let sut = getSut(maskAllText: true, maskAllImages: false) let layerIds = sut.getRedactLayerClassIdsTestOnly() @@ -96,6 +99,7 @@ class SentryLayerRedactionTests: SentryUIRedactBuilderTests { func testNoLayerClassesRegistered_whenBothMaskingDisabled() throws { guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let sut = getSut(maskAllText: false, maskAllImages: false) let layerIds = sut.getRedactLayerClassIdsTestOnly() @@ -105,6 +109,8 @@ class SentryLayerRedactionTests: SentryUIRedactBuilderTests { // MARK: - Layer-only redaction via mapRedactRegion func testRedact_layerOnlySublayer_shouldRedact() throws { + guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let (sut, rootView) = makeSutAndRootView(sublayerFrame: CGRect(x: 10, y: 10, width: 80, height: 20)) let result = sut.redactRegionsFor(view: rootView) @@ -113,7 +119,9 @@ class SentryLayerRedactionTests: SentryUIRedactBuilderTests { XCTAssertEqual(redactRegions.first?.size, CGSize(width: 80, height: 20)) } - func testRedact_layerOnlySublayer_notRedacted_whenClassNotRegistered() { + func testRedact_layerOnlySublayer_notRedacted_whenClassNotRegistered() throws { + guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let sut = getSut() // Don't call addRedactLayerClassIdTestOnly — DummyRedactableLayer is not registered let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) @@ -129,7 +137,9 @@ class SentryLayerRedactionTests: SentryUIRedactBuilderTests { // MARK: - Hidden / transparent layer-only sublayers - func testRedact_hiddenLayerOnlySublayer_shouldNotRedact() { + func testRedact_hiddenLayerOnlySublayer_shouldNotRedact() throws { + guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let (sut, rootView) = makeSutAndRootView() rootView.layer.sublayers?.last?.isHidden = true @@ -139,7 +149,9 @@ class SentryLayerRedactionTests: SentryUIRedactBuilderTests { XCTAssertEqual(redactRegions.count, 0, "Hidden layer-only sublayer should not be redacted") } - func testRedact_zeroOpacityLayerOnlySublayer_shouldNotRedact() { + func testRedact_zeroOpacityLayerOnlySublayer_shouldNotRedact() throws { + guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let (sut, rootView) = makeSutAndRootView() rootView.layer.sublayers?.last?.opacity = 0 @@ -151,7 +163,9 @@ class SentryLayerRedactionTests: SentryUIRedactBuilderTests { // MARK: - Unknown layer types should not be redacted - func testRedact_unknownLayerOnlySublayer_shouldNotRedact() { + func testRedact_unknownLayerOnlySublayer_shouldNotRedact() throws { + guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) let sublayer = CALayer() sublayer.frame = CGRect(x: 10, y: 10, width: 60, height: 20) @@ -166,7 +180,9 @@ class SentryLayerRedactionTests: SentryUIRedactBuilderTests { // MARK: - Region type is .redact (not .redactSwiftUI) - func testRedact_layerOnlySublayer_producesRedactType_notRedactSwiftUI() { + func testRedact_layerOnlySublayer_producesRedactType_notRedactSwiftUI() throws { + guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let (sut, rootView) = makeSutAndRootView() let result = sut.redactRegionsFor(view: rootView) @@ -179,7 +195,9 @@ class SentryLayerRedactionTests: SentryUIRedactBuilderTests { // MARK: - enforceIgnore respected - func testRedact_layerOnlySublayer_notRedacted_whenParentUnmasked() { + func testRedact_layerOnlySublayer_notRedacted_whenParentUnmasked() throws { + guard #available(iOS 26.0, tvOS 26.0, *) else { throw XCTSkip("Layer-only redaction requires iOS 26+") } + let sut = getSut() sut.addRedactLayerClassIdTestOnly(DummyRedactableLayer.description()) diff --git a/scripts/.clang-format-version b/scripts/.clang-format-version index 30ed877837..6c9141564b 100644 --- a/scripts/.clang-format-version +++ b/scripts/.clang-format-version @@ -1 +1 @@ -22.1.2 +22.1.3 From 50297b58b12d145ddbcdb80fecfa0b64d7979fa5 Mon Sep 17 00:00:00 2001 From: Itay Brenner Date: Mon, 20 Apr 2026 17:06:30 -0300 Subject: [PATCH 4/4] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9f775ee9f..142de76c97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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