@@ -101,8 +101,8 @@ final class MaskCollector {
101101 }
102102
103103 // Returns `true` if a mask was emitted for this view (the caller should stop recursing).
104- func emitViewMask( view: UIView , layer: CALayer , viewType: AnyClass , effectiveFrame: CGRect , resolvedExplicitMask: Bool ? ) -> Bool {
105- let shouldMask = policy. shouldMask ( view, viewType: viewType, resolvedExplicitMask: resolvedExplicitMask)
104+ func emitViewMask( view: UIView , layer: CALayer , viewType: AnyClass , className : String , effectiveFrame: CGRect , resolvedExplicitMask: Bool ? ) -> Bool {
105+ let shouldMask = policy. shouldMask ( view, viewType: viewType, className : className , resolvedExplicitMask: resolvedExplicitMask)
106106
107107 if shouldMask, let mask = MaskGeometry . createMask ( rPresentation: rPresentation, layer: layer, scale: scale) {
108108 var operation = MaskOperation ( mask: mask, effectiveFrame: effectiveFrame)
@@ -133,26 +133,24 @@ final class MaskCollector {
133133 // backing UIView, so the UIView-based path can't see them. Match by layer class name
134134 // while still honouring an inherited or marker-area explicit state.
135135 // Returns `true` if a mask was emitted (the caller should stop recursing).
136- func emitLayerOnlyMask( layer: CALayer , effectiveFrame: CGRect , resolvedExplicitMask: Bool ? ) -> Bool {
137- let shouldMask = resolvedExplicitMask ?? policy. shouldMaskLayer ( layer )
136+ func emitLayerOnlyMask( layerClassName : String , layer: CALayer , effectiveFrame: CGRect , resolvedExplicitMask: Bool ? ) -> Bool {
137+ let shouldMask = resolvedExplicitMask ?? policy. shouldMaskLayer ( className : layerClassName )
138138 guard shouldMask, let mask = MaskGeometry . createMask ( rPresentation: rPresentation, layer: layer, scale: scale) else {
139139 return false
140140 }
141141 operations. append ( MaskOperation ( mask: mask, effectiveFrame: effectiveFrame) )
142142 return true
143143 }
144144
145- func visit( layer: CALayer , inheritedExplicitMask: Bool ? ) {
145+ func visit( layer: CALayer , layerClassName : String , inheritedExplicitMask: Bool ? ) {
146146 // On iOS 26+, CameraUI private CALayer subclasses (e.g. ModeLoupeLayer) do not
147147 // implement init(layer:). Guard at the very top — before ANY property access —
148148 // because even isHidden/opacity access can trigger CA::Layer::presentation_layer()
149- // on a layer that lacks the initializer. The same guard is applied at every
150- // sublayer iteration site so CameraUI layers are never passed to visit() at all;
151- // this check is belt-and-suspenders for any path not covered there.
152- if NSStringFromClass ( type ( of: layer) ) . hasPrefix ( " CameraUI " ) { return }
149+ // on a layer that lacks the initializer. `layerClassName` is computed once by
150+ // the caller so we never call NSStringFromClass twice for the same layer.
151+ if policy. shouldSkipLayer ( className: layerClassName) { return }
153152
154153 guard !layer. isHidden, layer. opacity >= policy. minimumAlpha else { return }
155- if policy. shouldSkipLayer ( layer) { return }
156154
157155 // Frame in root coords is needed both for marker-area lookup
158156 // and for `effectiveFrame`/`MaskOperation`. Compute it once.
@@ -175,8 +173,9 @@ final class MaskCollector {
175173 }
176174
177175 let viewType : AnyClass = type ( of: view)
176+ let viewClassName = NSStringFromClass ( viewType)
178177
179- if policy. shouldIgnore ( view, viewType: viewType) || markerOverrideForLayer? . ignore == true {
178+ if policy. shouldIgnore ( view, viewType: viewType, className : viewClassName ) || markerOverrideForLayer? . ignore == true {
180179 return
181180 }
182181
@@ -186,7 +185,12 @@ final class MaskCollector {
186185 inheritedExplicitMask: inheritedExplicitMask,
187186 markerMask: markerOverrideForLayer? . mask
188187 )
189- if emitViewMask ( view: view, layer: layer, viewType: viewType, effectiveFrame: effectiveFrame, resolvedExplicitMask: resolvedExplicitMask) {
188+ if emitViewMask ( view: view,
189+ layer: layer,
190+ viewType: viewType,
191+ className: viewClassName,
192+ effectiveFrame: effectiveFrame,
193+ resolvedExplicitMask: resolvedExplicitMask) {
190194 return
191195 }
192196 childInheritedMask = resolvedExplicitMask
@@ -199,7 +203,7 @@ final class MaskCollector {
199203 } else {
200204 resolvedExplicitMask = inheritedExplicitMask ?? markerOverrideForLayer? . mask
201205 }
202- if emitLayerOnlyMask ( layer: layer, effectiveFrame: effectiveFrame, resolvedExplicitMask: resolvedExplicitMask) {
206+ if emitLayerOnlyMask ( layerClassName : layerClassName , layer: layer, effectiveFrame: effectiveFrame, resolvedExplicitMask: resolvedExplicitMask) {
203207 return
204208 }
205209 childInheritedMask = resolvedExplicitMask
@@ -215,31 +219,41 @@ final class MaskCollector {
215219 // layers avoids this. Mask positions may be slightly off during active
216220 // animations, but correctness under normal (non-animating) state is preserved.
217221 guard let modelSublayers = layer. model ( ) . sublayers, !modelSublayers. isEmpty else { return }
218- let safeSublayers = modelSublayers. filter {
219- !NSStringFromClass( type ( of: $0) ) . hasPrefix ( " CameraUI " )
222+ var safeSublayers : [ ( CALayer , String ) ] = [ ]
223+ safeSublayers. reserveCapacity ( modelSublayers. count)
224+ for sublayer in modelSublayers {
225+ let sublayerClassName = NSStringFromClass ( type ( of: sublayer) )
226+ if !policy. shouldSkipLayer ( className: sublayerClassName) {
227+ safeSublayers. append ( ( sublayer, sublayerClassName) )
228+ }
220229 }
221230 guard !safeSublayers. isEmpty else { return }
222231 if safeSublayers. count == 1 {
223- visit ( layer: safeSublayers [ 0 ] , inheritedExplicitMask: childInheritedMask)
232+ visit ( layer: safeSublayers [ 0 ] . 0 , layerClassName : safeSublayers [ 0 ] . 1 , inheritedExplicitMask: childInheritedMask)
224233 } else {
225- safeSublayers. sorted { $0. zPosition < $1. zPosition }
226- . forEach { visit ( layer: $0, inheritedExplicitMask: childInheritedMask) }
234+ safeSublayers. sorted { $0. 0 . zPosition < $1. 0 . zPosition }
235+ . forEach { visit ( layer: $0. 0 , layerClassName : $0 . 1 , inheritedExplicitMask: childInheritedMask) }
227236 }
228237 }
229238
230239 // Use model sublayers at the root level for the same reason: .sublayers on a
231240 // presentation layer creates presentation copies of all children, crashing on
232241 // iOS 26 if any child is or contains CameraUI.ModeLoupeLayer.
233242 let rootModelSublayers = rPresentation. model ( ) . sublayers ?? [ ]
234- let safeRootSublayers = rootModelSublayers. filter {
235- !NSStringFromClass( type ( of: $0) ) . hasPrefix ( " CameraUI " )
243+ var safeRootSublayers : [ ( CALayer , String ) ] = [ ]
244+ safeRootSublayers. reserveCapacity ( rootModelSublayers. count)
245+ for sublayer in rootModelSublayers {
246+ let sublayerClassName = NSStringFromClass ( type ( of: sublayer) )
247+ if !policy. shouldSkipLayer ( className: sublayerClassName) {
248+ safeRootSublayers. append ( ( sublayer, sublayerClassName) )
249+ }
236250 }
237251 if !safeRootSublayers. isEmpty {
238252 if safeRootSublayers. count == 1 {
239- visit ( layer: safeRootSublayers [ 0 ] , inheritedExplicitMask: nil )
253+ visit ( layer: safeRootSublayers [ 0 ] . 0 , layerClassName : safeRootSublayers [ 0 ] . 1 , inheritedExplicitMask: nil )
240254 } else {
241- safeRootSublayers. sorted { $0. zPosition < $1. zPosition }
242- . forEach { visit ( layer: $0, inheritedExplicitMask: nil ) }
255+ safeRootSublayers. sorted { $0. 0 . zPosition < $1. 0 . zPosition }
256+ . forEach { visit ( layer: $0. 0 , layerClassName : $0 . 1 , inheritedExplicitMask: nil ) }
243257 }
244258 }
245259
0 commit comments