@rejourneyco/react-native 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/android/build.gradle.kts +135 -0
  2. package/android/consumer-rules.pro +10 -0
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/main/AndroidManifest.xml +15 -0
  5. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
  6. package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
  7. package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
  8. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
  9. package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
  10. package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
  11. package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
  12. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
  13. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
  14. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
  15. package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
  16. package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
  17. package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
  18. package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
  19. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
  20. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
  21. package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
  22. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
  23. package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
  24. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
  25. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
  26. package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
  27. package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
  28. package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
  29. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
  30. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
  31. package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
  32. package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
  33. package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
  34. package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
  35. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
  36. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  37. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
  38. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  39. package/ios/Capture/RJANRHandler.h +42 -0
  40. package/ios/Capture/RJANRHandler.m +328 -0
  41. package/ios/Capture/RJCaptureEngine.h +275 -0
  42. package/ios/Capture/RJCaptureEngine.m +2062 -0
  43. package/ios/Capture/RJCaptureHeuristics.h +80 -0
  44. package/ios/Capture/RJCaptureHeuristics.m +903 -0
  45. package/ios/Capture/RJCrashHandler.h +46 -0
  46. package/ios/Capture/RJCrashHandler.m +313 -0
  47. package/ios/Capture/RJMotionEvent.h +183 -0
  48. package/ios/Capture/RJMotionEvent.m +183 -0
  49. package/ios/Capture/RJPerformanceManager.h +100 -0
  50. package/ios/Capture/RJPerformanceManager.m +373 -0
  51. package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
  52. package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
  53. package/ios/Capture/RJSegmentUploader.h +146 -0
  54. package/ios/Capture/RJSegmentUploader.m +778 -0
  55. package/ios/Capture/RJVideoEncoder.h +247 -0
  56. package/ios/Capture/RJVideoEncoder.m +1036 -0
  57. package/ios/Capture/RJViewControllerTracker.h +73 -0
  58. package/ios/Capture/RJViewControllerTracker.m +508 -0
  59. package/ios/Capture/RJViewHierarchyScanner.h +215 -0
  60. package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
  61. package/ios/Capture/RJViewSerializer.h +119 -0
  62. package/ios/Capture/RJViewSerializer.m +498 -0
  63. package/ios/Core/RJConstants.h +124 -0
  64. package/ios/Core/RJConstants.m +88 -0
  65. package/ios/Core/RJLifecycleManager.h +85 -0
  66. package/ios/Core/RJLifecycleManager.m +308 -0
  67. package/ios/Core/RJLogger.h +61 -0
  68. package/ios/Core/RJLogger.m +211 -0
  69. package/ios/Core/RJTypes.h +176 -0
  70. package/ios/Core/RJTypes.m +66 -0
  71. package/ios/Core/Rejourney.h +64 -0
  72. package/ios/Core/Rejourney.mm +2495 -0
  73. package/ios/Network/RJDeviceAuthManager.h +94 -0
  74. package/ios/Network/RJDeviceAuthManager.m +967 -0
  75. package/ios/Network/RJNetworkMonitor.h +68 -0
  76. package/ios/Network/RJNetworkMonitor.m +267 -0
  77. package/ios/Network/RJRetryManager.h +73 -0
  78. package/ios/Network/RJRetryManager.m +325 -0
  79. package/ios/Network/RJUploadManager.h +267 -0
  80. package/ios/Network/RJUploadManager.m +2296 -0
  81. package/ios/Privacy/RJPrivacyMask.h +163 -0
  82. package/ios/Privacy/RJPrivacyMask.m +922 -0
  83. package/ios/Rejourney.h +63 -0
  84. package/ios/Touch/RJGestureClassifier.h +130 -0
  85. package/ios/Touch/RJGestureClassifier.m +333 -0
  86. package/ios/Touch/RJTouchInterceptor.h +169 -0
  87. package/ios/Touch/RJTouchInterceptor.m +772 -0
  88. package/ios/Utils/RJEventBuffer.h +112 -0
  89. package/ios/Utils/RJEventBuffer.m +358 -0
  90. package/ios/Utils/RJGzipUtils.h +33 -0
  91. package/ios/Utils/RJGzipUtils.m +89 -0
  92. package/ios/Utils/RJKeychainManager.h +48 -0
  93. package/ios/Utils/RJKeychainManager.m +111 -0
  94. package/ios/Utils/RJPerfTiming.h +209 -0
  95. package/ios/Utils/RJPerfTiming.m +264 -0
  96. package/ios/Utils/RJTelemetry.h +92 -0
  97. package/ios/Utils/RJTelemetry.m +320 -0
  98. package/ios/Utils/RJWindowUtils.h +66 -0
  99. package/ios/Utils/RJWindowUtils.m +133 -0
  100. package/lib/commonjs/NativeRejourney.js +40 -0
  101. package/lib/commonjs/components/Mask.js +79 -0
  102. package/lib/commonjs/index.js +1381 -0
  103. package/lib/commonjs/sdk/autoTracking.js +1259 -0
  104. package/lib/commonjs/sdk/constants.js +151 -0
  105. package/lib/commonjs/sdk/errorTracking.js +199 -0
  106. package/lib/commonjs/sdk/index.js +50 -0
  107. package/lib/commonjs/sdk/metricsTracking.js +204 -0
  108. package/lib/commonjs/sdk/navigation.js +151 -0
  109. package/lib/commonjs/sdk/networkInterceptor.js +412 -0
  110. package/lib/commonjs/sdk/utils.js +363 -0
  111. package/lib/commonjs/types/expo-router.d.js +2 -0
  112. package/lib/commonjs/types/index.js +2 -0
  113. package/lib/module/NativeRejourney.js +38 -0
  114. package/lib/module/components/Mask.js +72 -0
  115. package/lib/module/index.js +1284 -0
  116. package/lib/module/sdk/autoTracking.js +1233 -0
  117. package/lib/module/sdk/constants.js +145 -0
  118. package/lib/module/sdk/errorTracking.js +189 -0
  119. package/lib/module/sdk/index.js +12 -0
  120. package/lib/module/sdk/metricsTracking.js +187 -0
  121. package/lib/module/sdk/navigation.js +143 -0
  122. package/lib/module/sdk/networkInterceptor.js +401 -0
  123. package/lib/module/sdk/utils.js +342 -0
  124. package/lib/module/types/expo-router.d.js +2 -0
  125. package/lib/module/types/index.js +2 -0
  126. package/lib/typescript/NativeRejourney.d.ts +147 -0
  127. package/lib/typescript/components/Mask.d.ts +39 -0
  128. package/lib/typescript/index.d.ts +117 -0
  129. package/lib/typescript/sdk/autoTracking.d.ts +204 -0
  130. package/lib/typescript/sdk/constants.d.ts +120 -0
  131. package/lib/typescript/sdk/errorTracking.d.ts +32 -0
  132. package/lib/typescript/sdk/index.d.ts +9 -0
  133. package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
  134. package/lib/typescript/sdk/navigation.d.ts +33 -0
  135. package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
  136. package/lib/typescript/sdk/utils.d.ts +148 -0
  137. package/lib/typescript/types/index.d.ts +624 -0
  138. package/package.json +102 -0
  139. package/rejourney.podspec +21 -0
  140. package/src/NativeRejourney.ts +165 -0
  141. package/src/components/Mask.tsx +80 -0
  142. package/src/index.ts +1459 -0
  143. package/src/sdk/autoTracking.ts +1373 -0
  144. package/src/sdk/constants.ts +134 -0
  145. package/src/sdk/errorTracking.ts +231 -0
  146. package/src/sdk/index.ts +11 -0
  147. package/src/sdk/metricsTracking.ts +232 -0
  148. package/src/sdk/navigation.ts +157 -0
  149. package/src/sdk/networkInterceptor.ts +440 -0
  150. package/src/sdk/utils.ts +369 -0
  151. package/src/types/expo-router.d.ts +7 -0
  152. package/src/types/index.ts +739 -0
@@ -0,0 +1,1464 @@
1
+ //
2
+ // RJViewHierarchyScanner.m
3
+ // Rejourney
4
+ //
5
+ // Unified view hierarchy scanner implementation.
6
+ // Combines layout signature generation and privacy rect detection
7
+ // into a single traversal pass for optimal performance.
8
+ //
9
+ // Licensed under the Apache License, Version 2.0 (the "License").
10
+ // Copyright (c) 2026 Rejourney
11
+ //
12
+
13
+ #import "RJViewHierarchyScanner.h"
14
+ #import "../Core/RJLogger.h"
15
+ #import <AVFoundation/AVFoundation.h>
16
+ #import <CommonCrypto/CommonDigest.h>
17
+ #import <UIKit/UIKit.h>
18
+ #import <WebKit/WebKit.h>
19
+ #import <objc/message.h>
20
+
21
+ static const CGFloat kRJScrollEpsilon = 0.5;
22
+ static const CGFloat kRJZoomEpsilon = 0.01;
23
+ static const CGFloat kRJInsetEpsilon = 0.5;
24
+ static const CGFloat kRJAnimationPresentationEpsilon = 1.0;
25
+
26
+ typedef struct {
27
+ double latitude;
28
+ double longitude;
29
+ } RJCoordinate;
30
+
31
+ typedef struct {
32
+ double latitudeDelta;
33
+ double longitudeDelta;
34
+ } RJCoordinateSpan;
35
+
36
+ static inline uint64_t fnv1a_u64(uint64_t h, const void *data, size_t len) {
37
+ const uint8_t *p = data;
38
+ while (len--) {
39
+ h ^= *p++;
40
+ h *= 1099511628211ULL;
41
+ }
42
+ return h;
43
+ }
44
+
45
+ #pragma mark - Scan Result Implementation
46
+
47
+ @implementation RJViewHierarchyScanResult
48
+
49
+ - (instancetype)init {
50
+ self = [super init];
51
+ if (self) {
52
+ _textInputFrames = @[];
53
+ _cameraFrames = @[];
54
+ _webViewFrames = @[];
55
+ _videoFrames = @[];
56
+ _mapViewFrames = @[];
57
+ _mapViewPointers = @[];
58
+ _scrollViewPointers = @[];
59
+ _animatedViewPointers = @[];
60
+ _scrollActive = NO;
61
+ _bounceActive = NO;
62
+ _refreshActive = NO;
63
+ _mapActive = NO;
64
+ _hasAnyAnimations = NO;
65
+ _animationAreaRatio = 0.0;
66
+ _didBailOutEarly = NO;
67
+ _totalViewsScanned = 0;
68
+ _scanTimestamp = [[NSDate date] timeIntervalSince1970];
69
+ }
70
+ return self;
71
+ }
72
+
73
+ - (BOOL)hasWebViews {
74
+ return self.webViewFrames.count > 0;
75
+ }
76
+
77
+ - (BOOL)hasTextInputs {
78
+ return self.textInputFrames.count > 0;
79
+ }
80
+
81
+ - (BOOL)hasCameraViews {
82
+ return self.cameraFrames.count > 0;
83
+ }
84
+
85
+ @end
86
+
87
+ @interface RJScrollViewState : NSObject
88
+
89
+ @property(nonatomic, assign) CGPoint contentOffset;
90
+ @property(nonatomic, assign) UIEdgeInsets contentInset;
91
+ @property(nonatomic, assign) CGFloat zoomScale;
92
+
93
+ @end
94
+
95
+ @implementation RJScrollViewState
96
+ @end
97
+
98
+ #pragma mark - Scanner Configuration Implementation
99
+
100
+ @implementation RJViewHierarchyScannerConfig
101
+
102
+ + (instancetype)defaultConfig {
103
+ RJViewHierarchyScannerConfig *config =
104
+ [[RJViewHierarchyScannerConfig alloc] init];
105
+ config.detectTextInputs = YES;
106
+ config.detectCameraViews = YES;
107
+ config.detectWebViews = YES;
108
+ config.detectVideoLayers = YES;
109
+ config.maskedNativeIDs = [NSSet set];
110
+ config.maxDepth = 8; // Aggressive optimization
111
+ config.maxViewCount = 500;
112
+ return config;
113
+ }
114
+
115
+ - (instancetype)init {
116
+ self = [super init];
117
+ if (self) {
118
+ _detectTextInputs = YES;
119
+ _detectCameraViews = YES;
120
+ _detectWebViews = YES;
121
+ _detectVideoLayers = YES;
122
+ _maskedNativeIDs = [NSSet set];
123
+ _maxDepth = 25; // Increased scan depth for better fidelity
124
+ _maxViewCount = 2000; // Increased view count limit
125
+ }
126
+ return self;
127
+ }
128
+
129
+ @end
130
+
131
+ #pragma mark - Private Interface
132
+
133
+ @interface RJViewHierarchyScanner ()
134
+
135
+ @property(nonatomic, strong) NSSet<NSString *> *rnInputClasses;
136
+
137
+ @property(nonatomic, strong) NSSet<NSString *> *cameraClasses;
138
+ @property(nonatomic, strong) NSSet<NSString *> *webViewClasses;
139
+ @property(nonatomic, strong) NSSet<NSString *> *mapViewClasses;
140
+
141
+ @property(nonatomic, assign) BOOL foundMapView;
142
+
143
+ @property(nonatomic, strong) NSArray<Class> *resolvedRnInputClasses;
144
+ @property(nonatomic, strong) NSArray<Class> *resolvedCameraClasses;
145
+ @property(nonatomic, strong) NSArray<Class> *resolvedWebViewClasses;
146
+
147
+ @property(nonatomic, assign) uint64_t layoutSignatureHash;
148
+
149
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *mutableTextInputFrames;
150
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *mutableCameraFrames;
151
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *mutableWebViewFrames;
152
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *mutableVideoFrames;
153
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *mutableMapViewFrames;
154
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *mutableMapViewPointers;
155
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *mutableScrollViewPointers;
156
+ @property(nonatomic, strong) NSMutableArray<NSValue *> *mutableAnimatedViewPointers;
157
+
158
+ @property(nonatomic, strong) NSMapTable<Class, NSString *> *classNameCache;
159
+
160
+ @property(nonatomic, assign) Class cachedUITextField;
161
+ @property(nonatomic, assign) Class cachedUITextView;
162
+ @property(nonatomic, assign) Class cachedUISearchBar;
163
+ @property(nonatomic, assign) Class cachedUIControl;
164
+ @property(nonatomic, assign) Class cachedUIImageView;
165
+ @property(nonatomic, assign) Class cachedUILabel;
166
+ @property(nonatomic, assign) Class cachedAVCapturePreviewLayer;
167
+ @property(nonatomic, assign) Class cachedAVPlayerLayer;
168
+
169
+ @property(nonatomic, assign) NSUInteger viewCount;
170
+
171
+ @property(nonatomic, assign) BOOL scanScrollActive;
172
+ @property(nonatomic, assign) BOOL scanBounceActive;
173
+ @property(nonatomic, assign) BOOL scanRefreshActive;
174
+ @property(nonatomic, assign) BOOL scanMapActive;
175
+ @property(nonatomic, assign) BOOL scanHasAnimations;
176
+ @property(nonatomic, assign) CGFloat scanAnimatedArea;
177
+
178
+ @property(nonatomic, strong)
179
+ NSMapTable<UIScrollView *, RJScrollViewState *> *scrollStateCache;
180
+ @property(nonatomic, strong) NSMapTable<UIView *, NSString *> *mapStateCache;
181
+
182
+ /// Whether the main scan bailed out early due to depth, view count, or time
183
+ @property(nonatomic, assign) BOOL didBailOutEarly;
184
+
185
+ /// Timestamp when scan started (for time-based bailout)
186
+ @property(nonatomic, assign) CFAbsoluteTime scanStartTime;
187
+
188
+ /// Maximum scan time in seconds before bailout (default: 30ms)
189
+ @property(nonatomic, assign) CFTimeInterval maxScanTime;
190
+
191
+ /// Primary capture window (for multi-window coordinate conversion)
192
+ /// When set, all sensitive view frames are converted to this window's
193
+ /// coordinate space
194
+ @property(nonatomic, weak) UIWindow *primaryCaptureWindow;
195
+
196
+ - (void)scanView:(UIView *)view
197
+ inWindow:(UIWindow *)window
198
+ depth:(NSInteger)depth;
199
+ - (void)scanSensitiveViewsOnlyInWindow:(UIWindow *)window;
200
+ - (BOOL)isMapViewLoading:(UIView *)view;
201
+ - (BOOL)isMapViewGestureActive:(UIView *)view;
202
+ - (BOOL)hasPresentationDeltaForView:(UIView *)view;
203
+
204
+ @end
205
+
206
+ #pragma mark - Implementation
207
+
208
+ @implementation RJViewHierarchyScanner
209
+
210
+ #pragma mark - Initialization
211
+
212
+ - (instancetype)init {
213
+ return [self initWithConfig:[RJViewHierarchyScannerConfig defaultConfig]];
214
+ }
215
+
216
+ - (instancetype)initWithConfig:(RJViewHierarchyScannerConfig *)config {
217
+ self = [super init];
218
+ if (self) {
219
+ _config = config;
220
+ _maxScanTime = 0.030; // 30ms soft cap for scan time
221
+
222
+ _rnInputClasses = [NSSet setWithArray:@[
223
+ @"RCTUITextField",
224
+ @"RCTBaseTextInputView",
225
+ @"RCTSinglelineTextInputView",
226
+ @"RCTMultilineTextInputView",
227
+ ]];
228
+
229
+ _cameraClasses = [NSSet setWithArray:@[
230
+ @"AVCaptureVideoPreviewView",
231
+ @"CameraView",
232
+ @"CKCameraView",
233
+ @"RNCameraView",
234
+ @"VisionCameraView",
235
+ @"RNVisionCameraView",
236
+ @"ExpoCameraView",
237
+ ]];
238
+
239
+ _webViewClasses = [NSSet setWithArray:@[
240
+ @"WKWebView",
241
+ @"UIWebView",
242
+ @"RCTWebView",
243
+ @"RNCWebView",
244
+ @"RNCWKWebView",
245
+ @"RCTWKWebView",
246
+ @"RNWebView",
247
+ @"WKContentView",
248
+ ]];
249
+
250
+ // MapView classes - when present, frame caching should be disabled
251
+ // because map tiles load asynchronously and layout signature doesn't
252
+ // capture them
253
+ _mapViewClasses = [NSSet setWithArray:@[
254
+ @"MKMapView", // Apple Maps
255
+ @"AIRMap", // react-native-maps (iOS)
256
+ @"AIRMapView", // react-native-maps alternate
257
+ @"RNMMapView", // react-native-maps newer versions
258
+ @"GMSMapView", // Google Maps SDK
259
+ ]];
260
+
261
+ _layoutSignatureHash = 14695981039346656037ULL;
262
+ _mutableTextInputFrames = [NSMutableArray arrayWithCapacity:8];
263
+ _mutableCameraFrames = [NSMutableArray arrayWithCapacity:2];
264
+ _mutableWebViewFrames = [NSMutableArray arrayWithCapacity:2];
265
+ _mutableVideoFrames = [NSMutableArray arrayWithCapacity:2];
266
+ _mutableMapViewFrames = [NSMutableArray arrayWithCapacity:2];
267
+ _mutableMapViewPointers = [NSMutableArray arrayWithCapacity:2];
268
+ _mutableScrollViewPointers = [NSMutableArray arrayWithCapacity:4];
269
+ _mutableAnimatedViewPointers = [NSMutableArray arrayWithCapacity:4];
270
+
271
+ _classNameCache = [NSMapTable strongToStrongObjectsMapTable];
272
+ _cachedUITextField = [UITextField class];
273
+ _cachedUITextView = [UITextView class];
274
+ _cachedUISearchBar = [UISearchBar class];
275
+ _cachedUIControl = [UIControl class];
276
+ _cachedUIImageView = [UIImageView class];
277
+ _cachedUILabel = [UILabel class];
278
+ _cachedAVCapturePreviewLayer = [AVCaptureVideoPreviewLayer class];
279
+ _cachedAVPlayerLayer = [AVPlayerLayer class];
280
+ _scrollStateCache = [NSMapTable weakToStrongObjectsMapTable];
281
+ _mapStateCache = [NSMapTable weakToStrongObjectsMapTable];
282
+ }
283
+ return self;
284
+ }
285
+
286
+ #pragma mark - Public API
287
+
288
+ - (RJViewHierarchyScanResult *)scanWindow:(UIWindow *)window {
289
+ if (!window) {
290
+ return nil;
291
+ }
292
+
293
+ @try {
294
+
295
+ self.layoutSignatureHash = 14695981039346656037ULL;
296
+ [self.mutableTextInputFrames removeAllObjects];
297
+ [self.mutableCameraFrames removeAllObjects];
298
+ [self.mutableWebViewFrames removeAllObjects];
299
+ [self.mutableVideoFrames removeAllObjects];
300
+ [self.mutableMapViewFrames removeAllObjects];
301
+ [self.mutableMapViewPointers removeAllObjects];
302
+ [self.mutableScrollViewPointers removeAllObjects];
303
+ [self.mutableAnimatedViewPointers removeAllObjects];
304
+ self.viewCount = 0;
305
+ self.foundMapView = NO; // Reset MapView detection
306
+ self.scanScrollActive = NO;
307
+ self.scanBounceActive = NO;
308
+ self.scanRefreshActive = NO;
309
+ self.scanMapActive = NO;
310
+ self.scanHasAnimations = NO;
311
+ self.scanAnimatedArea = 0.0;
312
+ self.primaryCaptureWindow =
313
+ nil; // Clear multi-window state for single-window scan
314
+ self.didBailOutEarly = NO;
315
+
316
+ // Record scan start time for time-based bailout
317
+ self.scanStartTime = CFAbsoluteTimeGetCurrent();
318
+
319
+ [self scanView:window inWindow:window depth:0];
320
+
321
+ // FAIL-CLOSED PRIVACY FALLBACK:
322
+ // On very complex screens, the main traversal may hit maxViewCount/maxDepth
323
+ // before encountering TextInputs, resulting in 0 textInputFrames and thus
324
+ // no masking. If that happens, run a lightweight targeted traversal that
325
+ // ONLY looks for sensitive views (text inputs / camera / manual masks).
326
+ //
327
+ // This keeps the fast path fast, but prevents privacy masking from silently
328
+ // failing on large view trees (e.g., complex RN screens with many
329
+ // subviews).
330
+ BOOL hitViewLimit = (self.viewCount >= self.config.maxViewCount);
331
+ BOOL needsPrivacyFallback =
332
+ (self.config.detectTextInputs &&
333
+ self.mutableTextInputFrames.count == 0) ||
334
+ (self.config.detectCameraViews && self.mutableCameraFrames.count == 0) ||
335
+ (self.config.detectWebViews && self.mutableWebViewFrames.count == 0) ||
336
+ (self.config.detectVideoLayers && self.mutableVideoFrames.count == 0);
337
+ if (needsPrivacyFallback && (hitViewLimit || self.didBailOutEarly)) {
338
+ [self scanSensitiveViewsOnlyInWindow:window];
339
+ }
340
+
341
+ RJViewHierarchyScanResult *result =
342
+ [[RJViewHierarchyScanResult alloc] init];
343
+ result.totalViewsScanned = self.viewCount;
344
+ result.textInputFrames = [self.mutableTextInputFrames copy];
345
+ result.cameraFrames = [self.mutableCameraFrames copy];
346
+ result.webViewFrames = [self.mutableWebViewFrames copy];
347
+ result.videoFrames = [self.mutableVideoFrames copy];
348
+ result.hasMapView =
349
+ self.foundMapView; // MapView detection for cache invalidation
350
+ result.mapViewFrames = [self.mutableMapViewFrames copy];
351
+ result.mapViewPointers = [self.mutableMapViewPointers copy];
352
+ result.scrollViewPointers = [self.mutableScrollViewPointers copy];
353
+ result.animatedViewPointers = [self.mutableAnimatedViewPointers copy];
354
+ result.scrollActive = self.scanScrollActive;
355
+ result.bounceActive = self.scanBounceActive;
356
+ result.refreshActive = self.scanRefreshActive;
357
+ result.mapActive = self.scanMapActive;
358
+ result.hasAnyAnimations = self.scanHasAnimations;
359
+ CGFloat screenArea = window.bounds.size.width * window.bounds.size.height;
360
+ result.animationAreaRatio =
361
+ (screenArea > 0)
362
+ ? MIN(self.scanAnimatedArea / screenArea, 1.0)
363
+ : 0.0;
364
+ result.didBailOutEarly = self.didBailOutEarly;
365
+
366
+ if (self.layoutSignatureHash != 14695981039346656037ULL) {
367
+ result.layoutSignature =
368
+ [NSString stringWithFormat:@"%016llx", self.layoutSignatureHash];
369
+ }
370
+
371
+ #ifdef DEBUG
372
+ CFTimeInterval scanDuration =
373
+ CFAbsoluteTimeGetCurrent() - self.scanStartTime;
374
+ if (scanDuration > 0.005) { // Log if scan took > 5ms
375
+ RJLogWarning(@"ViewHierarchyScanner: Slow scan - %lu views in %.1fms",
376
+ (unsigned long)result.totalViewsScanned,
377
+ scanDuration * 1000);
378
+ }
379
+
380
+ if (result.textInputFrames.count > 0 || result.cameraFrames.count > 0) {
381
+ RJLogDebug(
382
+ @"ViewHierarchyScanner: %lu views, %lu text inputs, %lu cameras",
383
+ (unsigned long)result.totalViewsScanned,
384
+ (unsigned long)result.textInputFrames.count,
385
+ (unsigned long)result.cameraFrames.count);
386
+ }
387
+ #endif
388
+
389
+ return result;
390
+ } @catch (NSException *exception) {
391
+ RJLogWarning(@"ViewHierarchyScanner: Scan failed: %@", exception);
392
+ return nil;
393
+ }
394
+ }
395
+
396
+ - (nullable RJViewHierarchyScanResult *)scanAllWindowsRelativeTo:
397
+ (UIWindow *)primaryWindow {
398
+ if (!primaryWindow) {
399
+ return nil;
400
+ }
401
+
402
+ // If no windows provided, scan all connected scenes (expensive fallback)
403
+ NSMutableArray<UIWindow *> *windowsToScan = [NSMutableArray array];
404
+
405
+ if (@available(iOS 13.0, *)) {
406
+ for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
407
+ if (![scene isKindOfClass:[UIWindowScene class]])
408
+ continue;
409
+ UIWindowScene *windowScene = (UIWindowScene *)scene;
410
+
411
+ // Filter out non-active scenes if possible, but be careful with
412
+ // modals
413
+ if (windowScene.activationState == UISceneActivationStateBackground ||
414
+ windowScene.activationState == UISceneActivationStateUnattached) {
415
+ continue;
416
+ }
417
+
418
+ for (UIWindow *window in windowScene.windows) {
419
+ if (!window.isHidden && window.alpha > 0.01) {
420
+ [windowsToScan addObject:window];
421
+ }
422
+ }
423
+ }
424
+ } else {
425
+ // Fallback for older iOS (unlikely needed but good for safety)
426
+ if (primaryWindow) {
427
+ [windowsToScan addObject:primaryWindow];
428
+ }
429
+ }
430
+
431
+ return [self scanWindows:windowsToScan relativeToWindow:primaryWindow];
432
+ }
433
+
434
+ - (nullable RJViewHierarchyScanResult *)scanWindows:
435
+ (NSArray<UIWindow *> *)windows
436
+ relativeToWindow:(UIWindow *)primaryWindow {
437
+ if (!primaryWindow || !windows) {
438
+ return nil;
439
+ }
440
+ if (windows.count == 0) {
441
+ return nil;
442
+ }
443
+
444
+ @try {
445
+ // Reset state for new frame
446
+ self.viewCount = 0;
447
+ self.layoutSignatureHash = 14695981039346656037ULL; // FNV-1a offset basis
448
+
449
+ // Reset other mutable properties
450
+ [self.mutableTextInputFrames removeAllObjects];
451
+ [self.mutableCameraFrames removeAllObjects];
452
+ [self.mutableWebViewFrames removeAllObjects];
453
+ [self.mutableVideoFrames removeAllObjects];
454
+ [self.mutableMapViewFrames removeAllObjects];
455
+ [self.mutableMapViewPointers removeAllObjects];
456
+ [self.mutableScrollViewPointers removeAllObjects];
457
+ [self.mutableAnimatedViewPointers removeAllObjects];
458
+ self.foundMapView = NO;
459
+ self.scanScrollActive = NO;
460
+ self.scanBounceActive = NO;
461
+ self.scanRefreshActive = NO;
462
+ self.scanMapActive = NO;
463
+ self.scanHasAnimations = NO;
464
+ self.scanAnimatedArea = 0.0;
465
+ self.didBailOutEarly = NO;
466
+ self.primaryCaptureWindow =
467
+ primaryWindow; // Store for coordinate conversion
468
+
469
+ // Record scan start time for time-based bailout
470
+ self.scanStartTime = CFAbsoluteTimeGetCurrent();
471
+
472
+ // Sort windows by level
473
+ NSMutableArray<UIWindow *> *windowsToScan = [windows mutableCopy];
474
+ [windowsToScan
475
+ sortUsingComparator:^NSComparisonResult(UIWindow *w1, UIWindow *w2) {
476
+ if (w1.windowLevel > w2.windowLevel)
477
+ return NSOrderedAscending;
478
+ if (w1.windowLevel < w2.windowLevel)
479
+ return NSOrderedDescending;
480
+ return NSOrderedSame;
481
+ }];
482
+
483
+ RJLogDebug(
484
+ @"ViewHierarchyScanner: Scanning %lu windows for sensitive views",
485
+ (unsigned long)windowsToScan.count);
486
+
487
+ // Scan each window
488
+ for (UIWindow *window in windowsToScan) {
489
+ // Scan this window's view hierarchy
490
+ // checkSensitiveView will use primaryCaptureWindow for coordinate
491
+ // conversion
492
+ [self scanView:window inWindow:window depth:0];
493
+
494
+ // If we're over limits, trigger privacy fallback for this window
495
+ BOOL hitViewLimit = (self.viewCount >= self.config.maxViewCount);
496
+ BOOL needsPrivacyFallback =
497
+ (self.config.detectTextInputs &&
498
+ self.mutableTextInputFrames.count == 0) ||
499
+ (self.config.detectCameraViews &&
500
+ self.mutableCameraFrames.count == 0) ||
501
+ (self.config.detectWebViews && self.mutableWebViewFrames.count == 0) ||
502
+ (self.config.detectVideoLayers &&
503
+ self.mutableVideoFrames.count == 0);
504
+ if (needsPrivacyFallback && hitViewLimit) {
505
+ [self scanSensitiveViewsOnlyInWindow:window];
506
+ }
507
+ }
508
+
509
+ // If the main scan bailed out early and found no inputs, do a targeted
510
+ // privacy-only scan as a fail-closed fallback.
511
+ BOOL needsPrivacyFallback =
512
+ (self.config.detectTextInputs &&
513
+ self.mutableTextInputFrames.count == 0) ||
514
+ (self.config.detectCameraViews && self.mutableCameraFrames.count == 0) ||
515
+ (self.config.detectWebViews && self.mutableWebViewFrames.count == 0) ||
516
+ (self.config.detectVideoLayers && self.mutableVideoFrames.count == 0);
517
+ if (needsPrivacyFallback && self.didBailOutEarly) {
518
+ for (UIWindow *window in windowsToScan) {
519
+ [self scanSensitiveViewsOnlyInWindow:window];
520
+ }
521
+ }
522
+
523
+ // Build result - frames are already converted to primaryWindow
524
+ // coordinates by checkSensitiveView (using self.primaryCaptureWindow)
525
+ RJViewHierarchyScanResult *result =
526
+ [[RJViewHierarchyScanResult alloc] init];
527
+ result.totalViewsScanned = self.viewCount;
528
+ result.textInputFrames = [self.mutableTextInputFrames copy];
529
+ result.cameraFrames = [self.mutableCameraFrames copy];
530
+ result.webViewFrames = [self.mutableWebViewFrames copy];
531
+ result.videoFrames = [self.mutableVideoFrames copy];
532
+ result.hasMapView = self.foundMapView;
533
+ result.mapViewFrames = [self.mutableMapViewFrames copy];
534
+ result.mapViewPointers = [self.mutableMapViewPointers copy];
535
+ result.scrollViewPointers = [self.mutableScrollViewPointers copy];
536
+ result.animatedViewPointers = [self.mutableAnimatedViewPointers copy];
537
+ result.scrollActive = self.scanScrollActive;
538
+ result.bounceActive = self.scanBounceActive;
539
+ result.refreshActive = self.scanRefreshActive;
540
+ result.mapActive = self.scanMapActive;
541
+ result.hasAnyAnimations = self.scanHasAnimations;
542
+ CGFloat screenArea =
543
+ primaryWindow.bounds.size.width * primaryWindow.bounds.size.height;
544
+ result.animationAreaRatio =
545
+ (screenArea > 0)
546
+ ? MIN(self.scanAnimatedArea / screenArea, 1.0)
547
+ : 0.0;
548
+ result.didBailOutEarly = self.didBailOutEarly;
549
+
550
+ if (self.layoutSignatureHash != 14695981039346656037ULL) {
551
+ result.layoutSignature =
552
+ [NSString stringWithFormat:@"%016llx", self.layoutSignatureHash];
553
+ }
554
+
555
+ // Clear primaryCaptureWindow after scan is complete
556
+ self.primaryCaptureWindow = nil;
557
+
558
+ #ifdef DEBUG
559
+ CFTimeInterval scanDuration =
560
+ CFAbsoluteTimeGetCurrent() - self.scanStartTime;
561
+ RJLogDebug(@"ViewHierarchyScanner: Multi-window scan complete - %lu views, "
562
+ @"%lu text inputs, %lu cameras in %.1fms",
563
+ (unsigned long)result.totalViewsScanned,
564
+ (unsigned long)result.textInputFrames.count,
565
+ (unsigned long)result.cameraFrames.count, scanDuration * 1000);
566
+ #endif
567
+
568
+ return result;
569
+ } @catch (NSException *exception) {
570
+ self.primaryCaptureWindow = nil; // Clean up on error
571
+ RJLogWarning(@"ViewHierarchyScanner: Multi-window scan failed: %@",
572
+ exception);
573
+ // Fallback to single-window scan
574
+ return [self scanWindow:primaryWindow];
575
+ }
576
+ }
577
+
578
+ - (BOOL)isViewVisible:(UIView *)view {
579
+ return !view.hidden && view.alpha > 0.01 && view.frame.size.width > 0 &&
580
+ view.frame.size.height > 0;
581
+ }
582
+
583
+ - (void)prewarmClassCaches {
584
+ // Pre-warm Objective-C runtime class lookups to eliminate first-scan
585
+ // penalty. NSClassFromString and class hierarchy checks are expensive on
586
+ // first call
587
+ // (~10-15ms total). By doing them here we front-load the cost before
588
+ // capture.
589
+
590
+ // Force resolution of all React Native input classes
591
+ NSMutableArray *rnInputs = [NSMutableArray array];
592
+ for (NSString *className in self.rnInputClasses) {
593
+ Class c = NSClassFromString(className);
594
+ if (c)
595
+ [rnInputs addObject:c];
596
+ }
597
+ self.resolvedRnInputClasses = rnInputs;
598
+
599
+ // Force resolution of all camera/video classes
600
+ NSMutableArray *cameras = [NSMutableArray array];
601
+ for (NSString *className in self.cameraClasses) {
602
+ Class c = NSClassFromString(className);
603
+ if (c)
604
+ [cameras addObject:c];
605
+ }
606
+ self.resolvedCameraClasses = cameras;
607
+
608
+ // Force resolution of all WebView classes
609
+ NSMutableArray *webViews = [NSMutableArray array];
610
+ for (NSString *className in self.webViewClasses) {
611
+ Class c = NSClassFromString(className);
612
+ if (c) {
613
+ [webViews addObject:c];
614
+ }
615
+ }
616
+ self.resolvedWebViewClasses = webViews;
617
+
618
+ // Pre-warm UIKit class hierarchy checks
619
+ // These trigger internal class resolution and method caching
620
+ UIView *dummyView = [[UIView alloc] init];
621
+ (void)[dummyView isKindOfClass:self.cachedUITextField];
622
+ (void)[dummyView isKindOfClass:self.cachedUITextView];
623
+ (void)[dummyView isKindOfClass:self.cachedUISearchBar];
624
+ (void)[dummyView isKindOfClass:self.cachedUIControl];
625
+ (void)[dummyView isKindOfClass:self.cachedUIImageView];
626
+ (void)[dummyView isKindOfClass:self.cachedUILabel];
627
+
628
+ // Pre-warm CALayer class checks
629
+ CALayer *dummyLayer = [[CALayer alloc] init];
630
+ (void)[dummyLayer isKindOfClass:self.cachedAVCapturePreviewLayer];
631
+ (void)[dummyLayer isKindOfClass:self.cachedAVPlayerLayer];
632
+
633
+ RJLogDebug(@"ViewHierarchyScanner: Class caches pre-warmed");
634
+ }
635
+
636
+ #pragma mark - Private Traversal
637
+
638
+ - (void)scanView:(UIView *)view
639
+ inWindow:(UIWindow *)window
640
+ depth:(NSInteger)depth {
641
+ if (!view || !window)
642
+ return;
643
+
644
+ // Early exit if we've scanned too many views (prevents runaway on complex
645
+ // UIs)
646
+ if (self.viewCount >= self.config.maxViewCount) {
647
+ self.didBailOutEarly = YES;
648
+ return;
649
+ }
650
+
651
+ if (depth > self.config.maxDepth) {
652
+ self.didBailOutEarly = YES;
653
+ return;
654
+ }
655
+
656
+ // TIME-BASED BAILOUT: Check every 200 views if we've exceeded max scan time
657
+ // This is a safety net for truly runaway scans, not normal operation
658
+ if (self.viewCount > 0 && (self.viewCount % 200) == 0) {
659
+ CFTimeInterval elapsed = CFAbsoluteTimeGetCurrent() - self.scanStartTime;
660
+ if (elapsed > self.maxScanTime) {
661
+ self.didBailOutEarly = YES;
662
+ RJLogWarning(@"ViewHierarchyScanner: Time bailout at %lu views (%.1fms > "
663
+ @"%.1fms limit)",
664
+ (unsigned long)self.viewCount, elapsed * 1000,
665
+ self.maxScanTime * 1000);
666
+ return;
667
+ }
668
+ }
669
+
670
+ if (![self isViewVisible:view])
671
+ return;
672
+
673
+ // REMOVED: [self.scannedViews containsObject:viewPtr] check
674
+ // Trees are acyclic in UIKit (usually), so we save the set lookups.
675
+ // If we really need cycle detection, restore it, but B2 optimization
676
+ // advises removing.
677
+
678
+ self.viewCount++;
679
+
680
+ @try {
681
+ BOOL isWebView = self.config.detectWebViews && [self isWebView:view];
682
+ BOOL isCamera = self.config.detectCameraViews && [self isCameraPreview:view];
683
+ BOOL isVideo = self.config.detectVideoLayers && [self isVideoLayerView:view];
684
+ BOOL isBlockedSurface = isWebView || isCamera || isVideo;
685
+
686
+ [self checkSensitiveView:view inWindow:window];
687
+
688
+ if ([view isKindOfClass:[UIScrollView class]]) {
689
+ [self trackScrollView:(UIScrollView *)view];
690
+ } else if (isWebView && [view respondsToSelector:@selector(scrollView)]) {
691
+ @try {
692
+ UIScrollView *webScrollView = [view valueForKey:@"scrollView"];
693
+ if ([webScrollView isKindOfClass:[UIScrollView class]]) {
694
+ [self trackScrollView:webScrollView];
695
+ }
696
+ } @catch (NSException *exception) {
697
+ }
698
+ }
699
+
700
+ if (isBlockedSurface) {
701
+ [self appendBlockedSurfaceInfoToSignature:view depth:depth];
702
+ return;
703
+ }
704
+
705
+ [self appendViewInfoToSignature:view depth:depth];
706
+ [self trackAnimationsForView:view inWindow:window];
707
+
708
+ NSArray *subviews = view.subviews;
709
+ if (subviews) {
710
+ for (UIView *subview in [subviews reverseObjectEnumerator]) {
711
+ @try {
712
+ [self scanView:subview inWindow:window depth:depth + 1];
713
+ } @catch (NSException *e) {
714
+ }
715
+ }
716
+ }
717
+ } @catch (NSException *exception) {
718
+ }
719
+ }
720
+
721
+ #pragma mark - Layout Signature Generation
722
+
723
+ - (void)mixInt:(int32_t)val {
724
+ self.layoutSignatureHash =
725
+ fnv1a_u64(self.layoutSignatureHash, &val, sizeof(val));
726
+ }
727
+
728
+ - (void)mixPtr:(const void *)ptr {
729
+ uintptr_t val = (uintptr_t)ptr;
730
+ self.layoutSignatureHash =
731
+ fnv1a_u64(self.layoutSignatureHash, &val, sizeof(val));
732
+ }
733
+
734
+ - (void)appendViewInfoToSignature:(UIView *)view depth:(NSInteger)depth {
735
+ if (!view)
736
+ return;
737
+
738
+ @try {
739
+ // 1. Mix depth
740
+ [self mixInt:(int32_t)depth];
741
+
742
+ // 2. Mix class pointer (faster than string name)
743
+ [self mixPtr:(__bridge const void *)[view class]];
744
+
745
+ // 3. Mix frame
746
+ CGRect f = view.frame;
747
+ int32_t x = (int32_t)lrintf(isfinite(f.origin.x) ? f.origin.x : 0);
748
+ int32_t y = (int32_t)lrintf(isfinite(f.origin.y) ? f.origin.y : 0);
749
+ int32_t w = (int32_t)lrintf(isfinite(f.size.width) ? f.size.width : 0);
750
+ int32_t h = (int32_t)lrintf(isfinite(f.size.height) ? f.size.height : 0);
751
+ [self mixInt:x];
752
+ [self mixInt:y];
753
+ [self mixInt:w];
754
+ [self mixInt:h];
755
+
756
+ // 4. Mix scroll offset
757
+ if ([view isKindOfClass:[UIScrollView class]]) {
758
+ CGPoint o = ((UIScrollView *)view).contentOffset;
759
+ [self mixInt:(int32_t)lrintf(isfinite(o.x) ? o.x : 0)];
760
+ [self mixInt:(int32_t)lrintf(isfinite(o.y) ? o.y : 0)];
761
+
762
+ UIEdgeInsets inset = ((UIScrollView *)view).contentInset;
763
+ [self mixInt:(int32_t)lrintf(isfinite(inset.top) ? inset.top * 100 : 0)];
764
+ [self mixInt:(int32_t)lrintf(isfinite(inset.bottom) ? inset.bottom * 100
765
+ : 0)];
766
+ [self mixInt:(int32_t)lrintf(isfinite(inset.left) ? inset.left * 100 : 0)];
767
+ [self mixInt:(int32_t)lrintf(isfinite(inset.right) ? inset.right * 100
768
+ : 0)];
769
+ }
770
+
771
+ // 5. Mix Text Content (avoid input content; use length only)
772
+ if ([view isKindOfClass:[UITextField class]]) {
773
+ NSString *text = ((UITextField *)view).text;
774
+ [self mixInt:(int32_t)text.length];
775
+ } else if ([view isKindOfClass:[UITextView class]]) {
776
+ UITextView *textView = (UITextView *)view;
777
+ if (textView.isEditable) {
778
+ [self mixInt:(int32_t)textView.text.length];
779
+ } else if (textView.text.length > 0) {
780
+ NSUInteger th = textView.text.hash;
781
+ self.layoutSignatureHash =
782
+ fnv1a_u64(self.layoutSignatureHash, &th, sizeof(th));
783
+ }
784
+ } else if ([view isKindOfClass:[UILabel class]]) {
785
+ NSString *text = ((UILabel *)view).text;
786
+ if (text.length > 0) {
787
+ NSUInteger th = text.hash;
788
+ self.layoutSignatureHash =
789
+ fnv1a_u64(self.layoutSignatureHash, &th, sizeof(th));
790
+ }
791
+ }
792
+
793
+ // 6. Mix Accessibility Label (Critical for RN view recycling)
794
+ // React Native often recycles views or uses generic RCTViews for text
795
+ // containers but updates the accessibility label. This is a very cheap way
796
+ // to detect changes.
797
+ NSString *axLabel = view.accessibilityLabel;
798
+ if (axLabel.length > 0) {
799
+ NSUInteger axh = axLabel.hash;
800
+ self.layoutSignatureHash =
801
+ fnv1a_u64(self.layoutSignatureHash, &axh, sizeof(axh));
802
+ }
803
+
804
+ // 7. Mix Image pointer (was 6)
805
+ if ([view isKindOfClass:[UIImageView class]]) {
806
+ UIImage *img = ((UIImageView *)view).image;
807
+ if (img) {
808
+ [self mixPtr:(__bridge const void *)img];
809
+ }
810
+ }
811
+
812
+ // 7b. Mix Map camera state when available
813
+ if ([self isMapView:view]) {
814
+ NSString *mapSignature = [self mapSignatureForView:view];
815
+ if (mapSignature.length > 0) {
816
+ NSUInteger mh = mapSignature.hash;
817
+ self.layoutSignatureHash =
818
+ fnv1a_u64(self.layoutSignatureHash, &mh, sizeof(mh));
819
+ }
820
+ [self mixInt:(int32_t)([self isMapViewLoading:view] ? 1 : 0)];
821
+ }
822
+
823
+ // 8. Mix Background Color
824
+ if (view.backgroundColor) {
825
+ [self mixInt:(int32_t)[view.backgroundColor hash]];
826
+ }
827
+
828
+ // 8b. Mix Tint Color (captures template image/color changes)
829
+ if (view.tintColor) {
830
+ [self mixInt:(int32_t)[view.tintColor hash]];
831
+ }
832
+
833
+ // 9. Mix Alpha (to 1% precision)
834
+ [self mixInt:(int32_t)(view.alpha * 100)];
835
+
836
+ // 10. Mix Hidden State
837
+ [self mixInt:(int32_t)(view.hidden ? 1 : 0)];
838
+
839
+ } @catch (NSException *exception) {
840
+ }
841
+ }
842
+
843
+ - (void)appendBlockedSurfaceInfoToSignature:(UIView *)view depth:(NSInteger)depth {
844
+ if (!view) {
845
+ return;
846
+ }
847
+
848
+ @try {
849
+ [self mixInt:(int32_t)depth];
850
+ [self mixPtr:(__bridge const void *)[view class]];
851
+
852
+ CGRect f = view.frame;
853
+ int32_t x = (int32_t)lrintf(isfinite(f.origin.x) ? f.origin.x : 0);
854
+ int32_t y = (int32_t)lrintf(isfinite(f.origin.y) ? f.origin.y : 0);
855
+ int32_t w = (int32_t)lrintf(isfinite(f.size.width) ? f.size.width : 0);
856
+ int32_t h = (int32_t)lrintf(isfinite(f.size.height) ? f.size.height : 0);
857
+ [self mixInt:x];
858
+ [self mixInt:y];
859
+ [self mixInt:w];
860
+ [self mixInt:h];
861
+
862
+ [self mixInt:(int32_t)(view.alpha * 100)];
863
+ [self mixInt:(int32_t)(view.hidden ? 1 : 0)];
864
+ } @catch (NSException *exception) {
865
+ }
866
+ }
867
+
868
+ #pragma mark - Sensitive View Detection
869
+
870
+ - (void)checkSensitiveView:(UIView *)view inWindow:(UIWindow *)window {
871
+ BOOL isTextInput =
872
+ self.config.detectTextInputs && [self isActualTextInput:view];
873
+ BOOL isCamera = self.config.detectCameraViews && [self isCameraPreview:view];
874
+ BOOL isWebView = self.config.detectWebViews && [self isWebView:view];
875
+ BOOL isVideo = self.config.detectVideoLayers && [self isVideoLayerView:view];
876
+ BOOL isManuallyMasked = [self isManuallyMaskedView:view];
877
+
878
+ // Check for MapView - capture frame and pointer for hybrid capture strategy
879
+ BOOL isMapView = [self isMapView:view];
880
+
881
+ // Determine the target window for coordinate conversion
882
+ // If primaryCaptureWindow is set (multi-window scan), use it
883
+ // Otherwise fall back to the passed window parameter
884
+ UIWindow *targetWindow = self.primaryCaptureWindow ?: window;
885
+ if (!targetWindow) {
886
+ return;
887
+ }
888
+
889
+ if (isMapView) {
890
+ self.foundMapView = YES;
891
+
892
+ if ([self isMapViewLoading:view]) {
893
+ self.scanMapActive = YES;
894
+ }
895
+
896
+ if ([self isMapViewGestureActive:view]) {
897
+ self.scanMapActive = YES;
898
+ }
899
+
900
+ NSString *mapSignature = [self mapSignatureForView:view];
901
+ if (mapSignature.length > 0) {
902
+ NSString *previous = [self.mapStateCache objectForKey:view];
903
+ if (previous && ![previous isEqualToString:mapSignature]) {
904
+ self.scanMapActive = YES;
905
+ }
906
+ [self.mapStateCache setObject:mapSignature forKey:view];
907
+ }
908
+
909
+ // Capture the MapView frame and pointer for hybrid capture
910
+ // Always convert through screen coordinates for consistency
911
+ CGRect frameInScreen = CGRectZero;
912
+ CGRect frameInTarget = CGRectZero;
913
+ @try {
914
+ frameInScreen = [view convertRect:view.bounds toView:nil];
915
+ frameInTarget = [targetWindow convertRect:frameInScreen fromWindow:nil];
916
+ } @catch (NSException *exception) {
917
+ return;
918
+ }
919
+
920
+ if (!CGRectIsEmpty(frameInTarget) && frameInTarget.size.width > 10 &&
921
+ frameInTarget.size.height > 10 &&
922
+ CGRectIntersectsRect(frameInTarget, targetWindow.bounds)) {
923
+ [self.mutableMapViewFrames
924
+ addObject:[NSValue valueWithCGRect:frameInTarget]];
925
+ [self.mutableMapViewPointers
926
+ addObject:[NSValue valueWithNonretainedObject:view]];
927
+ }
928
+ }
929
+
930
+ if (isTextInput || isCamera || isWebView || isVideo || isManuallyMasked) {
931
+ // CRITICAL FIX: Always convert through screen coordinates
932
+ // This ensures correct coordinate conversion even when:
933
+ // 1. The view is in a different window (modal, overlay, React Native
934
+ // navigation)
935
+ // 2. Multi-window scanning is active (primaryCaptureWindow is set)
936
+ // 3. Windows have different transforms or scales
937
+ //
938
+ // Step 1: Convert view bounds to screen coordinates (toView:nil)
939
+ // Step 2: Convert screen coordinates to target window coordinates
940
+ // (fromWindow:nil)
941
+ CGRect frameInScreen = CGRectZero;
942
+ CGRect frameInTarget = CGRectZero;
943
+ @try {
944
+ frameInScreen = [view convertRect:view.bounds toView:nil];
945
+ frameInTarget = [targetWindow convertRect:frameInScreen fromWindow:nil];
946
+ } @catch (NSException *exception) {
947
+ return;
948
+ }
949
+
950
+ // If conversion failed (view not in window hierarchy), try direct
951
+ // conversion
952
+ if (CGRectIsEmpty(frameInTarget) ||
953
+ (frameInTarget.origin.x == 0 && frameInTarget.origin.y == 0 &&
954
+ frameInTarget.size.width == 0 && frameInTarget.size.height == 0)) {
955
+ // Fallback: try direct conversion if view is in target window
956
+ if (view.window == targetWindow) {
957
+ @try {
958
+ frameInTarget = [view convertRect:view.bounds toView:targetWindow];
959
+ } @catch (NSException *exception) {
960
+ return;
961
+ }
962
+ }
963
+ }
964
+
965
+ // Sanitize NaN values before storing to prevent CoreGraphics errors
966
+ CGFloat x = isnan(frameInTarget.origin.x) ? 0 : frameInTarget.origin.x;
967
+ CGFloat y = isnan(frameInTarget.origin.y) ? 0 : frameInTarget.origin.y;
968
+ CGFloat w = isnan(frameInTarget.size.width) ? 0 : frameInTarget.size.width;
969
+ CGFloat h =
970
+ isnan(frameInTarget.size.height) ? 0 : frameInTarget.size.height;
971
+
972
+ // Also check for infinity values
973
+ if (isinf(x))
974
+ x = 0;
975
+ if (isinf(y))
976
+ y = 0;
977
+ if (isinf(w))
978
+ w = 0;
979
+ if (isinf(h))
980
+ h = 0;
981
+
982
+ CGRect sanitizedFrame = CGRectMake(x, y, w, h);
983
+
984
+ if (!CGRectIsEmpty(sanitizedFrame) && sanitizedFrame.size.width > 10 &&
985
+ sanitizedFrame.size.height > 10 &&
986
+ CGRectIntersectsRect(sanitizedFrame, targetWindow.bounds)) {
987
+
988
+ NSValue *frameValue = [NSValue valueWithCGRect:sanitizedFrame];
989
+
990
+ if (isCamera) {
991
+ [self.mutableCameraFrames addObject:frameValue];
992
+ } else if (isWebView) {
993
+ [self.mutableWebViewFrames addObject:frameValue];
994
+ } else if (isVideo) {
995
+ [self.mutableVideoFrames addObject:frameValue];
996
+ } else {
997
+ [self.mutableTextInputFrames addObject:frameValue];
998
+ }
999
+
1000
+ #ifdef DEBUG
1001
+ RJLogDebug(@"ViewHierarchyScanner: Found %@ at (%.0f,%.0f,%.0f,%.0f) - "
1002
+ @"view.window=%@ targetWindow=%@",
1003
+ isTextInput
1004
+ ? @"TextInput"
1005
+ : (isCamera
1006
+ ? @"Camera"
1007
+ : (isWebView ? @"WebView"
1008
+ : (isVideo ? @"Video" : @"MaskedView"))),
1009
+ sanitizedFrame.origin.x, sanitizedFrame.origin.y,
1010
+ sanitizedFrame.size.width, sanitizedFrame.size.height,
1011
+ NSStringFromClass([view.window class]),
1012
+ NSStringFromClass([targetWindow class]));
1013
+ #endif
1014
+ }
1015
+ }
1016
+ }
1017
+
1018
+ #pragma mark - Privacy Fallback Traversal
1019
+
1020
+ /// Targeted traversal that ONLY checks for sensitive views.
1021
+ /// Used when the main traversal hits view limits and finds no text inputs.
1022
+ - (void)scanSensitiveViewsOnlyInWindow:(UIWindow *)window {
1023
+ if (!window)
1024
+ return;
1025
+
1026
+ @try {
1027
+ CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
1028
+ const CFTimeInterval maxTime = 0.010; // 10ms budget (aggressive)
1029
+ const NSUInteger maxViews = 2000; // Reduced from 4000
1030
+
1031
+ NSMutableArray<UIView *> *queue = [NSMutableArray arrayWithCapacity:256];
1032
+ [queue addObject:window];
1033
+
1034
+ NSUInteger scanned = 0;
1035
+ NSUInteger headIndex = 0; // Optimization: Use index pointer (O(1) pop)
1036
+
1037
+ while (headIndex < queue.count) {
1038
+ if (scanned >= maxViews) {
1039
+ break;
1040
+ }
1041
+ if ((CFAbsoluteTimeGetCurrent() - start) > maxTime) {
1042
+ break;
1043
+ }
1044
+
1045
+ UIView *current = queue[headIndex++];
1046
+ if (!current)
1047
+ continue;
1048
+
1049
+ if ([self isViewVisible:current]) {
1050
+ [self checkSensitiveView:current inWindow:window];
1051
+ }
1052
+
1053
+ NSArray<UIView *> *subviews = current.subviews;
1054
+ if (subviews.count > 0) {
1055
+ [queue addObjectsFromArray:subviews];
1056
+ }
1057
+
1058
+ scanned++;
1059
+ }
1060
+
1061
+ #ifdef DEBUG
1062
+ if (self.mutableTextInputFrames.count > 0) {
1063
+ RJLogWarning(@"ViewHierarchyScanner: Privacy fallback found %lu text "
1064
+ @"inputs after main scan hit view limit",
1065
+ (unsigned long)self.mutableTextInputFrames.count);
1066
+ } else {
1067
+ RJLogWarning(@"ViewHierarchyScanner: Privacy fallback found 0 text "
1068
+ @"inputs (window=%@)",
1069
+ NSStringFromClass([window class]));
1070
+ }
1071
+ #endif
1072
+ } @catch (NSException *exception) {
1073
+ RJLogWarning(@"ViewHierarchyScanner: Privacy fallback failed: %@",
1074
+ exception);
1075
+ }
1076
+ }
1077
+
1078
+ #pragma mark - Class Name Caching
1079
+
1080
+ - (NSString *)cachedClassNameForClass:(Class)cls {
1081
+ NSString *cached = [self.classNameCache objectForKey:cls];
1082
+ if (cached)
1083
+ return cached;
1084
+
1085
+ NSString *name = NSStringFromClass(cls);
1086
+ [self.classNameCache setObject:name forKey:cls];
1087
+ return name;
1088
+ }
1089
+
1090
+ #pragma mark - Sensitive View Detection Helpers
1091
+
1092
+ - (BOOL)isActualTextInput:(UIView *)view {
1093
+ if (!view)
1094
+ return NO;
1095
+
1096
+ // Start with direct class checks for common inputs
1097
+ if ([view isKindOfClass:[UITextField class]] ||
1098
+ [view isKindOfClass:[UITextView class]] ||
1099
+ [view isKindOfClass:[UISearchBar class]]) {
1100
+ if ([view isKindOfClass:[UITextView class]]) {
1101
+ return ((UITextView *)view).isEditable;
1102
+ }
1103
+ return YES;
1104
+ }
1105
+
1106
+ // 2. Fast resolved-class check (RN inputs)
1107
+ for (Class cls in self.resolvedRnInputClasses) {
1108
+ if ([view isKindOfClass:cls]) {
1109
+ return YES;
1110
+ }
1111
+ }
1112
+
1113
+ // 3. Fallback: String heuristics (slow, only if needed)
1114
+ NSString *className = [self cachedClassNameForClass:[view class]];
1115
+ if ([className containsString:@"TextInput"] ||
1116
+ [className containsString:@"TextField"] ||
1117
+ [className containsString:@"TextEditor"]) {
1118
+ return YES;
1119
+ }
1120
+
1121
+ // NOTE: B5 Optimization recommends removing the superclass crawl if not
1122
+ // strictly needed. We'll leave the string check as the final fallback but
1123
+ // rely on resolved classes for 99% of cases.
1124
+
1125
+ return NO;
1126
+ }
1127
+
1128
+ - (BOOL)isCameraPreview:(UIView *)view {
1129
+ if (!view)
1130
+ return NO;
1131
+
1132
+ // 1. Fast resolved-class check
1133
+ for (Class cls in self.resolvedCameraClasses) {
1134
+ if ([view isKindOfClass:cls]) {
1135
+ return YES;
1136
+ }
1137
+ }
1138
+
1139
+ // 2. Fast layer check
1140
+ if ([view.layer isKindOfClass:self.cachedAVCapturePreviewLayer]) {
1141
+ return YES;
1142
+ }
1143
+
1144
+ for (CALayer *sublayer in view.layer.sublayers) {
1145
+ if ([sublayer isKindOfClass:self.cachedAVCapturePreviewLayer]) {
1146
+ return YES;
1147
+ }
1148
+ }
1149
+
1150
+ // 3. Fallback: String heuristics
1151
+ NSString *className = [self cachedClassNameForClass:[view class]];
1152
+ return [self.cameraClasses containsObject:className] ||
1153
+ [className containsString:@"Camera"] ||
1154
+ [className containsString:@"Preview"] ||
1155
+ [className containsString:@"Scanner"];
1156
+ }
1157
+
1158
+ - (BOOL)isWebView:(UIView *)view {
1159
+ if (!view)
1160
+ return NO;
1161
+
1162
+ Class wkClass = NSClassFromString(@"WKWebView");
1163
+ if (wkClass && [view isKindOfClass:wkClass]) {
1164
+ return YES;
1165
+ }
1166
+
1167
+ for (Class cls in self.resolvedWebViewClasses) {
1168
+ if ([view isKindOfClass:cls]) {
1169
+ return YES;
1170
+ }
1171
+ }
1172
+
1173
+ NSString *className = [self cachedClassNameForClass:[view class]];
1174
+ return [self.webViewClasses containsObject:className] ||
1175
+ [className containsString:@"WebView"] ||
1176
+ [className containsString:@"WKContentView"];
1177
+ }
1178
+
1179
+ - (BOOL)isVideoLayerView:(UIView *)view {
1180
+ if (!view)
1181
+ return NO;
1182
+
1183
+ if ([view.layer isKindOfClass:self.cachedAVPlayerLayer]) {
1184
+ return YES;
1185
+ }
1186
+
1187
+ for (CALayer *sublayer in view.layer.sublayers) {
1188
+ if ([sublayer isKindOfClass:self.cachedAVPlayerLayer]) {
1189
+ return YES;
1190
+ }
1191
+ }
1192
+
1193
+ NSString *className = [self cachedClassNameForClass:[view class]];
1194
+ return ([className containsString:@"Video"] &&
1195
+ [className containsString:@"View"]);
1196
+ }
1197
+
1198
+ - (BOOL)isMapView:(UIView *)view {
1199
+ if (!view)
1200
+ return NO;
1201
+
1202
+ NSString *className = [self cachedClassNameForClass:[view class]];
1203
+ return [self.mapViewClasses containsObject:className] ||
1204
+ [className containsString:@"MapView"] ||
1205
+ [className containsString:@"MKMapView"];
1206
+ }
1207
+
1208
+ - (BOOL)isMapViewLoading:(UIView *)view {
1209
+ if (!view) {
1210
+ return NO;
1211
+ }
1212
+
1213
+ @try {
1214
+ if ([view respondsToSelector:@selector(isLoading)]) {
1215
+ BOOL (*loadingMsg)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
1216
+ return loadingMsg(view, @selector(isLoading));
1217
+ }
1218
+ id loadingValue = [view valueForKey:@"loading"];
1219
+ if ([loadingValue respondsToSelector:@selector(boolValue)]) {
1220
+ return [loadingValue boolValue];
1221
+ }
1222
+ } @catch (NSException *exception) {
1223
+ }
1224
+
1225
+ return NO;
1226
+ }
1227
+
1228
+ - (BOOL)isMapViewGestureActive:(UIView *)view {
1229
+ if (!view) {
1230
+ return NO;
1231
+ }
1232
+
1233
+ for (UIGestureRecognizer *recognizer in view.gestureRecognizers) {
1234
+ if (recognizer.state == UIGestureRecognizerStateBegan ||
1235
+ recognizer.state == UIGestureRecognizerStateChanged ||
1236
+ recognizer.state == UIGestureRecognizerStateEnded) {
1237
+ return YES;
1238
+ }
1239
+ }
1240
+
1241
+ return NO;
1242
+ }
1243
+
1244
+ - (BOOL)hasPresentationDeltaForView:(UIView *)view {
1245
+ CALayer *presentation = view.layer.presentationLayer;
1246
+ if (!presentation) {
1247
+ return NO;
1248
+ }
1249
+
1250
+ CGRect modelFrame = view.layer.frame;
1251
+ CGRect presentationFrame = presentation.frame;
1252
+
1253
+ if (!isfinite(modelFrame.origin.x) || !isfinite(modelFrame.origin.y) ||
1254
+ !isfinite(modelFrame.size.width) || !isfinite(modelFrame.size.height) ||
1255
+ !isfinite(presentationFrame.origin.x) ||
1256
+ !isfinite(presentationFrame.origin.y) ||
1257
+ !isfinite(presentationFrame.size.width) ||
1258
+ !isfinite(presentationFrame.size.height)) {
1259
+ return NO;
1260
+ }
1261
+
1262
+ CGFloat dx = fabs(presentationFrame.origin.x - modelFrame.origin.x);
1263
+ CGFloat dy = fabs(presentationFrame.origin.y - modelFrame.origin.y);
1264
+ CGFloat dw = fabs(presentationFrame.size.width - modelFrame.size.width);
1265
+ CGFloat dh = fabs(presentationFrame.size.height - modelFrame.size.height);
1266
+
1267
+ return (dx > kRJAnimationPresentationEpsilon ||
1268
+ dy > kRJAnimationPresentationEpsilon ||
1269
+ dw > kRJAnimationPresentationEpsilon ||
1270
+ dh > kRJAnimationPresentationEpsilon);
1271
+ }
1272
+
1273
+ - (BOOL)isManuallyMaskedView:(UIView *)view {
1274
+ if ([view.accessibilityHint isEqualToString:@"rejourney_occlude"]) {
1275
+ return YES;
1276
+ }
1277
+
1278
+ NSString *nativeID = view.accessibilityIdentifier;
1279
+ if (nativeID.length > 0 &&
1280
+ [self.config.maskedNativeIDs containsObject:nativeID]) {
1281
+ return YES;
1282
+ }
1283
+
1284
+ NSArray *subviews = view.subviews;
1285
+ for (UIView *subview in [subviews reverseObjectEnumerator]) {
1286
+ NSString *childNativeID = subview.accessibilityIdentifier;
1287
+ if (childNativeID.length > 0 &&
1288
+ [self.config.maskedNativeIDs containsObject:childNativeID]) {
1289
+ return YES;
1290
+ }
1291
+ }
1292
+
1293
+ return NO;
1294
+ }
1295
+
1296
+ #pragma mark - Motion & Animation Tracking
1297
+
1298
+ - (void)trackScrollView:(UIScrollView *)scrollView {
1299
+ if (!scrollView) {
1300
+ return;
1301
+ }
1302
+
1303
+ [self.mutableScrollViewPointers
1304
+ addObject:[NSValue valueWithNonretainedObject:scrollView]];
1305
+
1306
+ RJScrollViewState *state = [self.scrollStateCache objectForKey:scrollView];
1307
+ if (!state) {
1308
+ state = [[RJScrollViewState alloc] init];
1309
+ }
1310
+
1311
+ CGPoint offset = scrollView.contentOffset;
1312
+ UIEdgeInsets inset = scrollView.contentInset;
1313
+ CGFloat zoomScale = scrollView.zoomScale;
1314
+
1315
+ BOOL tracking = scrollView.isTracking || scrollView.isDragging ||
1316
+ scrollView.isDecelerating;
1317
+ BOOL offsetMoved = (fabs(offset.x - state.contentOffset.x) > kRJScrollEpsilon ||
1318
+ fabs(offset.y - state.contentOffset.y) > kRJScrollEpsilon);
1319
+ BOOL zoomMoved = fabs(zoomScale - state.zoomScale) > kRJZoomEpsilon;
1320
+ if (tracking || offsetMoved || zoomMoved) {
1321
+ self.scanScrollActive = YES;
1322
+ }
1323
+
1324
+ BOOL insetChanged =
1325
+ (fabs(inset.top - state.contentInset.top) > kRJInsetEpsilon ||
1326
+ fabs(inset.bottom - state.contentInset.bottom) > kRJInsetEpsilon ||
1327
+ fabs(inset.left - state.contentInset.left) > kRJInsetEpsilon ||
1328
+ fabs(inset.right - state.contentInset.right) > kRJInsetEpsilon);
1329
+ if ([self isOverscrolling:scrollView offset:offset] || insetChanged) {
1330
+ self.scanBounceActive = YES;
1331
+ }
1332
+
1333
+ if ([self isRefreshActiveForScrollView:scrollView
1334
+ offset:offset
1335
+ inset:inset]) {
1336
+ self.scanRefreshActive = YES;
1337
+ }
1338
+
1339
+ state.contentOffset = offset;
1340
+ state.contentInset = inset;
1341
+ state.zoomScale = zoomScale;
1342
+ [self.scrollStateCache setObject:state forKey:scrollView];
1343
+ }
1344
+
1345
+ - (BOOL)isOverscrolling:(UIScrollView *)scrollView offset:(CGPoint)offset {
1346
+ UIEdgeInsets inset = UIEdgeInsetsZero;
1347
+ @try {
1348
+ inset = scrollView.adjustedContentInset;
1349
+ } @catch (NSException *exception) {
1350
+ inset = scrollView.contentInset;
1351
+ }
1352
+ CGFloat topLimit = -inset.top - kRJScrollEpsilon;
1353
+ CGFloat bottomLimit = scrollView.contentSize.height -
1354
+ scrollView.bounds.size.height +
1355
+ inset.bottom + kRJScrollEpsilon;
1356
+ if (offset.y < topLimit || offset.y > bottomLimit) {
1357
+ return YES;
1358
+ }
1359
+
1360
+ CGFloat leftLimit = -inset.left - kRJScrollEpsilon;
1361
+ CGFloat rightLimit = scrollView.contentSize.width -
1362
+ scrollView.bounds.size.width +
1363
+ inset.right + kRJScrollEpsilon;
1364
+ return (offset.x < leftLimit || offset.x > rightLimit);
1365
+ }
1366
+
1367
+ - (BOOL)isRefreshActiveForScrollView:(UIScrollView *)scrollView
1368
+ offset:(CGPoint)offset
1369
+ inset:(UIEdgeInsets)inset {
1370
+ UIRefreshControl *refreshControl = scrollView.refreshControl;
1371
+ if (!refreshControl) {
1372
+ return NO;
1373
+ }
1374
+
1375
+ if (refreshControl.isRefreshing) {
1376
+ return YES;
1377
+ }
1378
+
1379
+ CGFloat triggerOffset = -scrollView.adjustedContentInset.top -
1380
+ kRJScrollEpsilon;
1381
+ if (offset.y < triggerOffset) {
1382
+ return YES;
1383
+ }
1384
+
1385
+ if (refreshControl.superview) {
1386
+ CGRect inScroll = [refreshControl.superview convertRect:refreshControl.frame
1387
+ toView:scrollView];
1388
+ if (CGRectIntersectsRect(inScroll, scrollView.bounds)) {
1389
+ return YES;
1390
+ }
1391
+ }
1392
+
1393
+ return NO;
1394
+ }
1395
+
1396
+ - (void)trackAnimationsForView:(UIView *)view inWindow:(UIWindow *)window {
1397
+ if (!view) {
1398
+ return;
1399
+ }
1400
+
1401
+ BOOL hasAnimationKeys = (view.layer.animationKeys.count > 0);
1402
+ BOOL hasPresentationDelta = [self hasPresentationDeltaForView:view];
1403
+ if (!hasAnimationKeys && !hasPresentationDelta) {
1404
+ return;
1405
+ }
1406
+
1407
+ self.scanHasAnimations = YES;
1408
+ [self.mutableAnimatedViewPointers
1409
+ addObject:[NSValue valueWithNonretainedObject:view]];
1410
+
1411
+ UIWindow *targetWindow = self.primaryCaptureWindow ?: window;
1412
+ if (!targetWindow) {
1413
+ return;
1414
+ }
1415
+ CGRect frameInScreen = CGRectZero;
1416
+ CGRect frameInTarget = CGRectZero;
1417
+ @try {
1418
+ frameInScreen = [view convertRect:view.bounds toView:nil];
1419
+ frameInTarget = [targetWindow convertRect:frameInScreen fromWindow:nil];
1420
+ } @catch (NSException *exception) {
1421
+ return;
1422
+ }
1423
+
1424
+ if (CGRectIsEmpty(frameInTarget) ||
1425
+ !CGRectIntersectsRect(frameInTarget, targetWindow.bounds)) {
1426
+ return;
1427
+ }
1428
+
1429
+ CGRect visibleFrame = CGRectIntersection(frameInTarget, targetWindow.bounds);
1430
+ self.scanAnimatedArea += visibleFrame.size.width * visibleFrame.size.height;
1431
+ }
1432
+
1433
+ - (NSString *)mapSignatureForView:(UIView *)view {
1434
+ @try {
1435
+ NSValue *centerValue = [view valueForKey:@"centerCoordinate"];
1436
+ NSValue *spanValue = [view valueForKeyPath:@"region.span"];
1437
+
1438
+ RJCoordinate center = {0, 0};
1439
+ RJCoordinateSpan span = {0, 0};
1440
+ if ([centerValue isKindOfClass:[NSValue class]]) {
1441
+ [centerValue getValue:&center];
1442
+ }
1443
+ if ([spanValue isKindOfClass:[NSValue class]]) {
1444
+ [spanValue getValue:&span];
1445
+ }
1446
+
1447
+ NSNumber *altitude = [view valueForKeyPath:@"camera.altitude"];
1448
+ NSNumber *heading = [view valueForKeyPath:@"camera.heading"];
1449
+ NSNumber *pitch = [view valueForKeyPath:@"camera.pitch"];
1450
+
1451
+ double altitudeValue = altitude ? altitude.doubleValue : 0;
1452
+ double headingValue = heading ? heading.doubleValue : 0;
1453
+ double pitchValue = pitch ? pitch.doubleValue : 0;
1454
+
1455
+ return [NSString stringWithFormat:@"%.5f:%.5f:%.5f:%.5f:%.1f:%.1f:%.1f",
1456
+ center.latitude, center.longitude,
1457
+ span.latitudeDelta, span.longitudeDelta,
1458
+ altitudeValue, headingValue, pitchValue];
1459
+ } @catch (NSException *exception) {
1460
+ return @"";
1461
+ }
1462
+ }
1463
+
1464
+ @end