@@ -9,8 +9,18 @@ import WebKit
99#endif
1010
1111final class SentryUIRedactBuilder {
12+ // MARK: - Constants
13+
14+ /// Class identifier for ``CameraUI.ChromeSwiftUIView``, if it exists.
15+ ///
16+ /// This object identifier is used to identify views of this class type during the redaction process.
17+ /// This workaround is specifically for Xcode 16 building for iOS 26 where accessing CameraUI.ModeLoupeLayer
18+ /// causes a crash due to unimplemented init(layer:) initializer.
19+ private static let cameraSwiftUIViewClassId = " CameraUI.ChromeSwiftUIView "
20+
1221 ///This is a wrapper which marks it's direct children to be ignored
1322 private var ignoreContainerClassIdentifier : ObjectIdentifier ?
23+
1424 ///This is a wrapper which marks it's direct children to be redacted
1525 private var redactContainerClassIdentifier : ObjectIdentifier ?
1626
@@ -186,7 +196,7 @@ final class SentryUIRedactBuilder {
186196 }
187197
188198 private func shouldIgnore( view: UIView ) -> Bool {
189- return SentryRedactViewHelper . shouldUnmask ( view) || containsIgnoreClass ( type ( of: view) ) || shouldIgnoreParentContainer ( view)
199+ return SentryRedactViewHelper . shouldUnmask ( view) || containsIgnoreClass ( type ( of: view) ) || shouldIgnoreParentContainer ( view)
190200 }
191201
192202 private func shouldIgnoreParentContainer( _ view: UIView ) -> Bool {
@@ -228,6 +238,19 @@ final class SentryUIRedactBuilder {
228238 }
229239 let newTransform = concatenateTranform ( transform, from: layer, withParent: parentLayer)
230240
241+ // Check if the subtree should be ignored to avoid crashes with some special views.
242+ // If a subtree is ignored, it will be fully redacted and we return early to prevent duplicates.
243+ if isViewSubtreeIgnored ( view) {
244+ redacting. append ( SentryRedactRegion (
245+ size: layer. bounds. size,
246+ transform: newTransform,
247+ type: . redact,
248+ color: self . color ( for: view) ,
249+ name: view. debugDescription
250+ ) )
251+ return
252+ }
253+
231254 let ignore = !forceRedact && shouldIgnore ( view: view)
232255 let swiftUI = SentryRedactViewHelper . shouldRedactSwiftUI ( view)
233256 let redact = forceRedact || shouldRedact ( view: view) || swiftUI
@@ -239,7 +262,7 @@ final class SentryUIRedactBuilder {
239262 transform: newTransform,
240263 type: swiftUI ? . redactSwiftUI : . redact,
241264 color: self . color ( for: view) ,
242- name: layer . name ?? layer . debugDescription
265+ name: view . debugDescription
243266 ) )
244267
245268 guard !view. clipsToBounds else {
@@ -256,11 +279,12 @@ final class SentryUIRedactBuilder {
256279 size: layer. bounds. size,
257280 transform: newTransform,
258281 type: . clipOut,
259- name: layer . name ?? layer . debugDescription
282+ name: view . debugDescription
260283 ) )
261284 }
262285 }
263-
286+
287+ // Traverse the sublayers to redact them if necessary
264288 guard let subLayers = layer. sublayers, subLayers. count > 0 else {
265289 return
266290 }
@@ -272,7 +296,7 @@ final class SentryUIRedactBuilder {
272296 size: layer. bounds. size,
273297 transform: newTransform,
274298 type: . clipEnd,
275- name: layer . name ?? layer . debugDescription
299+ name: view . debugDescription
276300 ) )
277301 }
278302 for subLayer in subLayers. sorted ( by: { $0. zPosition < $1. zPosition } ) {
@@ -283,11 +307,40 @@ final class SentryUIRedactBuilder {
283307 size: layer. bounds. size,
284308 transform: newTransform,
285309 type: . clipBegin,
286- name: layer . name ?? layer . debugDescription
310+ name: view . debugDescription
287311 ) )
288312 }
289313 }
290314
315+ private func isViewSubtreeIgnored( _ view: UIView ) -> Bool {
316+ // We intentionally avoid using `NSClassFromString` or directly referencing class objects here,
317+ // because both approaches can trigger the Objective-C `+initialize` method on the class.
318+ // This has side effects and can cause crashes, especially when performed off the main thread
319+ // or with UIKit classes that expect to be initialized on the main thread.
320+ //
321+ // Instead, we use the string description of the type (i.e., `type(of: view).description()`)
322+ // for comparison. This is a safer, more "Swifty" approach that avoids the pitfalls of
323+ // class initialization side effects.
324+ //
325+ // We have previously encountered related issues:
326+ // - In EmergeTools' snapshotting code where using `NSClassFromString` led to crashes [1]
327+ // - In Sentry's own SubClassFinder where storing or accessing class objects on a background thread caused crashes due to `+initialize` being called on UIKit classes [2]
328+ //
329+ // [1] https://github.com/EmergeTools/SnapshotPreviews/blob/main/Sources/SnapshotPreviewsCore/View%2BSnapshot.swift#L248
330+ // [2] https://github.com/getsentry/sentry-cocoa/blob/00d97404946a37e983eabb21cc64bd3d5d2cb474/Sources/Sentry/SentrySubClassFinder.m#L58-L84
331+ let viewTypeId = type ( of: view) . description ( )
332+
333+ if #available( iOS 26 . 0 , * ) , viewTypeId == Self . cameraSwiftUIViewClassId {
334+ // CameraUI.ChromeSwiftUIView is a special case because it contains layers which can not be iterated due to this error:
335+ //
336+ // Fatal error: Use of unimplemented initializer 'init(layer:)' for class 'CameraUI.ModeLoupeLayer'
337+ //
338+ // This crash only occurs when building with Xcode 16 for iOS 26, so we add a runtime check
339+ return true
340+ }
341+ return false
342+ }
343+
291344 /**
292345 Gets a transform that represents the layer global position.
293346 */
0 commit comments