|
21 | 21 | private let urlInterceptor: URLSessionInterceptor
|
22 | 22 | private var sessionSwizzler: URLSessionSwizzler?
|
23 | 23 |
|
24 |
| - // SwiftUI image types |
25 |
| - // https://stackoverflow.com/questions/57554590/how-to-get-all-the-subviews-of-a-window-or-view-in-latest-swiftui-app |
26 |
| - // https://stackoverflow.com/questions/58336045/how-to-detect-swiftui-usage-programmatically-in-an-ios-application |
27 |
| - private let swiftUIImageTypes = ["SwiftUI._UIGraphicsView", |
28 |
| - "SwiftUI.ImageLayer"].compactMap { NSClassFromString($0) } |
29 |
| - |
30 |
| - private let swiftUIGenericTypes = ["_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", |
31 |
| - "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView"].compactMap { NSClassFromString($0) } |
| 24 | + /** |
| 25 | + ### Mapping of SwiftUI Views to UIKit |
| 26 | + |
| 27 | + This section summarizes findings on how SwiftUI views map to UIKit components |
| 28 | + |
| 29 | + #### Image-Based Views |
| 30 | + - **`AsyncImage` and `Image`** |
| 31 | + - Both views have a `CALayer` of type `SwiftUI.ImageLayer`. |
| 32 | + - The associated `UIView` is of type `SwiftUI._UIGraphicsView`. |
| 33 | + |
| 34 | + #### Graphic-based Views |
| 35 | + - **`Color`, `Divider`, `Gradient` etc |
| 36 | + - These are backed by `SwiftUI._UIGraphicsView` but have a different layer type than images |
| 37 | + |
| 38 | + #### Text-Based Views |
| 39 | + - **`Text`, `Button`, and `TextEditor`** |
| 40 | + - These views are backed by a `UIView` of type `SwiftUI.CGDrawingView`, which is a subclass of `SwiftUI._UIGraphicsView`. |
| 41 | + - CoreGraphics (`CG`) is used for rendering text content directly, making it challenging to access the value programmatically. |
| 42 | + |
| 43 | + #### UIKit-Mapped Views |
| 44 | + - **Views Hosted by `UIViewRepresentable`** |
| 45 | + - Some SwiftUI views map directly to UIKit classes or to a subclass: |
| 46 | + - **Control Images** (e.g., in `Picker` drop-downs) may map to `UIImageView`. |
| 47 | + - **Buttons** map to `SwiftUI.UIKitIconPreferringButton` (a subclass of `UIButton`). |
| 48 | + - **Toggle** maps to `UISwitch` (the toggle itself, excluding its label). |
| 49 | + - **Picker** with wheel style maps to `UIPickerView`. Other styles use combinations of image-based and text-based views. |
| 50 | + |
| 51 | + #### Layout and Structure Views |
| 52 | + - **`Spacer`, `VStack`, `HStack`, `ZStack`, and Lazy Stacks** |
| 53 | + - These views do not correspond to specific a `UIView`. Instead, they translate directly into layout constraints. |
| 54 | + |
| 55 | + #### List-Based Views |
| 56 | + - **`List` and Scrollable Container Views** |
| 57 | + - Backed by a subclass of `UICollectionView` |
| 58 | + |
| 59 | + #### Other SwiftUI Views |
| 60 | + - Most other SwiftUI views are *compositions* of the views described above |
| 61 | + |
| 62 | + SwiftUI Image Types: |
| 63 | + - [StackOverflow: Subviews of a Window or View in SwiftUI](https://stackoverflow.com/questions/57554590/how-to-get-all-the-subviews-of-a-window-or-view-in-latest-swiftui-app) |
| 64 | + - [StackOverflow: Detect SwiftUI Usage Programmatically](https://stackoverflow.com/questions/58336045/how-to-detect-swiftui-usage-programmatically-in-an-ios-application) |
| 65 | + */ |
| 66 | + |
| 67 | + /// `AsyncImage` and `Image` |
| 68 | + private let swiftUIImageLayerTypes = [ |
| 69 | + "SwiftUI.ImageLayer", |
| 70 | + ].compactMap(NSClassFromString) |
| 71 | + |
| 72 | + /// `Text`, `Button`, `TextEditor` views |
| 73 | + private let swiftUITextBasedViewTypes = [ |
| 74 | + "SwiftUI.CGDrawingView", // Text, Button |
| 75 | + "SwiftUI.TextEditorTextView", // TextEditor |
| 76 | + "SwiftUI.VerticalTextView", // TextField, vertical axis |
| 77 | + ].compactMap(NSClassFromString) |
| 78 | + |
| 79 | + private let swiftUIGenericTypes = [ |
| 80 | + "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", |
| 81 | + ].compactMap(NSClassFromString) |
32 | 82 |
|
33 | 83 | private let reactNativeTextView: AnyClass? = NSClassFromString("RCTTextView")
|
34 | 84 | private let reactNativeImageView: AnyClass? = NSClassFromString("RCTImageView")
|
|
171 | 221 | }
|
172 | 222 | }
|
173 | 223 |
|
174 |
| - if let textField = view as? UITextField { // TextField |
| 224 | + /// SwiftUI: `TextField`, `SecureField` will land here |
| 225 | + if let textField = view as? UITextField { |
175 | 226 | if isTextFieldSensitive(textField) {
|
176 | 227 | maskableWidgets.append(view.toAbsoluteRect(window))
|
177 | 228 | return
|
|
185 | 236 | }
|
186 | 237 | }
|
187 | 238 |
|
188 |
| - if let image = view as? UIImageView { // Image, this code might never be reachable in SwiftUI, see swiftUIImageTypes instead |
| 239 | + /// SwiftUI: Some control images like the ones in `Picker` view may land here |
| 240 | + if let image = view as? UIImageView { |
189 | 241 | if isImageViewSensitive(image) {
|
190 | 242 | maskableWidgets.append(view.toAbsoluteRect(window))
|
191 | 243 | return
|
|
215 | 267 | }
|
216 | 268 | }
|
217 | 269 |
|
218 |
| - if let button = view as? UIButton { // Button, this code might never be reachable in SwiftUI, see swiftUIImageTypes instead |
| 270 | + /// SwiftUI: `SwiftUI.UIKitIconPreferringButton` and other subclasses will land here |
| 271 | + if let button = view as? UIButton { |
219 | 272 | if isButtonSensitive(button) {
|
220 | 273 | maskableWidgets.append(view.toAbsoluteRect(window))
|
221 | 274 | return
|
222 | 275 | }
|
223 | 276 | }
|
224 | 277 |
|
225 |
| - if let theSwitch = view as? UISwitch { // Toggle (no text, items are just rendered to Text (swiftUIImageTypes)) |
| 278 | + /// SwiftUI: `Toggle` (no text, labels are just rendered to Text (swiftUIImageTypes)) |
| 279 | + if let theSwitch = view as? UISwitch { |
226 | 280 | if isSwitchSensitive(theSwitch) {
|
227 | 281 | maskableWidgets.append(view.toAbsoluteRect(window))
|
228 | 282 | return
|
|
232 | 286 | // if its a generic type and has subviews, subviews have to be checked first
|
233 | 287 | let hasSubViews = !view.subviews.isEmpty
|
234 | 288 |
|
235 |
| - if let picker = view as? UIPickerView { // Picker (no source, items are just rendered to Text (swiftUIImageTypes)) |
| 289 | + /// SwiftUI: `Picker` with .pickerStyle(.wheel) will land here |
| 290 | + if let picker = view as? UIPickerView { |
236 | 291 | if isTextInputSensitive(picker), !hasSubViews {
|
237 | 292 | maskableWidgets.append(picker.toAbsoluteRect(window))
|
238 | 293 | return
|
239 | 294 | }
|
240 | 295 | }
|
241 | 296 |
|
242 |
| - if swiftUIImageTypes.contains(where: { view.isKind(of: $0) }) { |
| 297 | + /// SwiftUI: Text based views like `Text`, `Button`, `TextEditor` |
| 298 | + if swiftUITextBasedViewTypes.contains(where: view.isKind(of:)) { |
| 299 | + if isTextInputSensitive(view), !hasSubViews { |
| 300 | + maskableWidgets.append(view.toAbsoluteRect(window)) |
| 301 | + return |
| 302 | + } |
| 303 | + } |
| 304 | + |
| 305 | + /// SwiftUI: Image based views like `Image`, `AsyncImage`. (Note: We check the layer type here) |
| 306 | + if swiftUIImageLayerTypes.contains(where: view.layer.isKind(of:)) { |
243 | 307 | if isSwiftUIImageSensitive(view), !hasSubViews {
|
244 | 308 | maskableWidgets.append(view.toAbsoluteRect(window))
|
245 | 309 | return
|
|
359 | 423 | }
|
360 | 424 |
|
361 | 425 | private func isSwiftUIImageSensitive(_ view: UIView) -> Bool {
|
362 |
| - // the raw type _UIGraphicsView is always something like Color.white or similar |
363 |
| - // never contains PII and should not be masked |
364 |
| - // Button will fall in this case case but Button has subviews |
365 |
| - let type = type(of: view) |
366 |
| - |
367 |
| - let rawGraphicsView = String(describing: type) == "_UIGraphicsView" |
368 |
| - return (config.sessionReplayConfig.maskAllImages || view.isNoCapture()) && !rawGraphicsView |
| 426 | + // No way of checking if this is an asset image or not |
| 427 | + // No way of checking if there's actual content in the image or not |
| 428 | + config.sessionReplayConfig.maskAllImages || view.isNoCapture() |
369 | 429 | }
|
370 | 430 |
|
371 | 431 | private func isImageViewSensitive(_ view: UIImageView) -> Bool {
|
|
0 commit comments