@onekeyfe/react-native-pager-view 3.0.158 → 3.0.159

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 (42) hide show
  1. package/README.md +57 -4
  2. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +379 -31
  3. package/android/src/main/java/com/reactnativepagerview/NativeScrollCoordinatorContributions.kt +15 -0
  4. package/android/src/main/java/com/reactnativepagerview/NativeScrollerPendingRestore.kt +26 -0
  5. package/android/src/main/java/com/reactnativepagerview/NativeScrollerScrollAwayInsets.kt +41 -0
  6. package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +600 -0
  7. package/android/src/main/java/com/reactnativepagerview/NativeScrollerViewManager.kt +145 -0
  8. package/android/src/main/java/com/reactnativepagerview/PagerViewViewPackage.kt +1 -1
  9. package/android/src/main/jni/CMakeLists.txt +19 -0
  10. package/android/src/main/jni/pagerview.h +11 -0
  11. package/android/src/main/res/values/native_scroll_coordinator_ids.xml +5 -0
  12. package/android/src/test/java/com/reactnativepagerview/NativeScrollCoordinatorContributionsTest.kt +26 -0
  13. package/android/src/test/java/com/reactnativepagerview/NativeScrollerPendingRestoreTest.kt +31 -0
  14. package/android/src/test/java/com/reactnativepagerview/NativeScrollerScrollAwayInsetsTest.kt +75 -0
  15. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerComponentDescriptor.h +8 -0
  16. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.cpp +15 -0
  17. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerShadowNode.h +23 -0
  18. package/common/cpp/react/renderer/components/pagerview/RNCNativeScrollerState.h +33 -0
  19. package/docs/SPEC.md +451 -0
  20. package/ios/RNCCollapsiblePagerReleasedStatePolicy.h +8 -0
  21. package/ios/RNCCollapsiblePagerReleasedStatePolicy.m +13 -0
  22. package/ios/RNCCollapsiblePagerViewComponentView.mm +420 -2
  23. package/ios/RNCNativeScrollerComponentView.h +17 -0
  24. package/ios/RNCNativeScrollerComponentView.mm +479 -0
  25. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +80 -0
  26. package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +788 -0
  27. package/ios/tests/RNCCollapsiblePagerPressCancellationWiringTests.mm +109 -0
  28. package/ios/tests/RNCCollapsiblePagerRefreshControlTests.mm +214 -0
  29. package/ios/tests/RNCCollapsiblePagerReleasedStatePolicyTests.mm +23 -0
  30. package/ios/tests/RNCNativeScrollerTests.mm +252 -0
  31. package/lib/module/CollapsiblePagerView.js +10 -1
  32. package/lib/module/NativeScroller.js +171 -11
  33. package/lib/module/NativeScrollerNativeComponent.ts +101 -0
  34. package/lib/typescript/src/CollapsiblePagerView.d.ts +3 -3
  35. package/lib/typescript/src/NativeScroller.d.ts +15 -7
  36. package/lib/typescript/src/NativeScrollerNativeComponent.d.ts +79 -0
  37. package/package.json +8 -4
  38. package/react-native-pager-view.podspec +2 -1
  39. package/react-native.config.js +15 -0
  40. package/src/CollapsiblePagerView.tsx +14 -4
  41. package/src/NativeScroller.tsx +291 -14
  42. package/src/NativeScrollerNativeComponent.ts +101 -0
@@ -0,0 +1,788 @@
1
+ #import <XCTest/XCTest.h>
2
+ #import <UIKit/UIGestureRecognizerSubclass.h>
3
+ #import <React/RCTSurfaceTouchHandler.h>
4
+ #import <React/RCTViewComponentView.h>
5
+ #import <dlfcn.h>
6
+ #import <mach/mach_time.h>
7
+ #import <objc/message.h>
8
+
9
+ #import "RNCCollapsiblePagerViewComponentView.h"
10
+ #import <react/renderer/components/pagerview/Props.h>
11
+
12
+ using namespace facebook::react;
13
+
14
+ #pragma mark - UIKit gesture environment and touch delivery
15
+
16
+ // Pod unit tests run in xctest without an application. UIKit then has no
17
+ // gesture environment: every programmatic recognizer state change, including
18
+ // Began on a plain UIGestureRecognizer, is dropped. Create the application
19
+ // singleton (what UIApplicationMain does first) so recognizers run under the
20
+ // real UIGestureEnvironment. Touches below are delivered through
21
+ // -[UIWindow sendEvent:], the path UIApplication uses for touch events, using
22
+ // the same private UITouch/UIEvent/IOHIDEvent calls as KIF. These are
23
+ // test-only; a missing symbol fails the test instead of skipping it.
24
+ @interface RNCPressCancellationTestApplication : UIApplication
25
+ @end
26
+
27
+ @implementation RNCPressCancellationTestApplication
28
+ // UIApplicationMain never ran, so there is no event run loop. UIScrollView
29
+ // pushes a tracking run-loop mode when its real pan begins; scheduling is
30
+ // outside the recognizer semantics under test.
31
+ - (void)_pushRunLoopMode:(id)mode requester:(id)requester reason:(id)reason {}
32
+ - (void)_popRunLoopMode:(id)mode requester:(id)requester reason:(id)reason {}
33
+ @end
34
+
35
+ static BOOL RNCEnsureUIKitGestureEnvironment(void)
36
+ {
37
+ if (UIApplication.sharedApplication != nil) return YES;
38
+ void (*instantiate)(Class) =
39
+ (void (*)(Class))dlsym(RTLD_DEFAULT, "UIApplicationInstantiateSingleton");
40
+ if (instantiate == NULL) return NO;
41
+ instantiate(RNCPressCancellationTestApplication.class);
42
+ return UIApplication.sharedApplication != nil;
43
+ }
44
+
45
+ typedef struct __IOHIDEvent *RNCTestHIDEventRef;
46
+
47
+ @interface UITouch (RNCPressCancellationSynthesis)
48
+ - (void)setWindow:(UIWindow *)window;
49
+ - (void)setView:(UIView *)view;
50
+ - (void)setGestureView:(UIView *)view;
51
+ - (void)setPhase:(UITouchPhase)phase;
52
+ - (void)setTimestamp:(NSTimeInterval)timestamp;
53
+ - (void)setTapCount:(NSUInteger)tapCount;
54
+ - (void)_setLocationInWindow:(CGPoint)location resetPrevious:(BOOL)resetPrevious;
55
+ - (void)_setIsTapToClick:(BOOL)isTapToClick;
56
+ - (void)_setHidEvent:(RNCTestHIDEventRef)event;
57
+ @end
58
+
59
+ @interface UIEvent (RNCPressCancellationSynthesis)
60
+ - (instancetype)_init;
61
+ - (void)_clearTouches;
62
+ - (void)_addTouch:(UITouch *)touch forDelayedDelivery:(BOOL)delayed;
63
+ - (void)_setHIDEvent:(RNCTestHIDEventRef)event;
64
+ @end
65
+
66
+ // Delivers one finger or several simultaneous fingers through a window.
67
+ @interface RNCSynthesizedTouchDriver : NSObject
68
+ - (instancetype)initWithWindow:(UIWindow *)window;
69
+ @property (nonatomic, readonly) NSString *unavailableReason;
70
+ - (UITouch *)beginAt:(CGPoint)windowPoint;
71
+ - (void)move:(UITouch *)touch to:(CGPoint)windowPoint;
72
+ - (void)end:(UITouch *)touch;
73
+ @end
74
+
75
+ @implementation RNCSynthesizedTouchDriver {
76
+ UIWindow *_window;
77
+ UIEvent *_event;
78
+ NSMutableArray<UITouch *> *_activeTouches;
79
+ NSTimeInterval _timestamp;
80
+ RNCTestHIDEventRef (*_createHand)(CFAllocatorRef, uint64_t, uint32_t, uint32_t, uint32_t,
81
+ uint32_t, uint32_t, double, double, double, double, double,
82
+ Boolean, Boolean, uint32_t);
83
+ RNCTestHIDEventRef (*_createFinger)(CFAllocatorRef, uint64_t, uint32_t, uint32_t, uint32_t,
84
+ double, double, double, double, double, double, double,
85
+ double, double, double, Boolean, Boolean, uint32_t);
86
+ void (*_setIntegerValue)(RNCTestHIDEventRef, uint32_t, long, uint32_t);
87
+ void (*_appendEvent)(RNCTestHIDEventRef, RNCTestHIDEventRef, uint32_t);
88
+ void (*_setSenderID)(RNCTestHIDEventRef, uint64_t);
89
+ }
90
+
91
+ - (instancetype)initWithWindow:(UIWindow *)window
92
+ {
93
+ if (self = [super init]) {
94
+ _window = window;
95
+ _activeTouches = [NSMutableArray new];
96
+ _timestamp = NSProcessInfo.processInfo.systemUptime;
97
+ void *iokit = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_NOW);
98
+ _createHand = (decltype(_createHand))dlsym(iokit, "IOHIDEventCreateDigitizerEvent");
99
+ _createFinger =
100
+ (decltype(_createFinger))dlsym(iokit, "IOHIDEventCreateDigitizerFingerEventWithQuality");
101
+ _setIntegerValue =
102
+ (decltype(_setIntegerValue))dlsym(iokit, "IOHIDEventSetIntegerValueWithOptions");
103
+ _appendEvent = (decltype(_appendEvent))dlsym(iokit, "IOHIDEventAppendEvent");
104
+ _setSenderID = (decltype(_setSenderID))dlsym(iokit, "IOHIDEventSetSenderID");
105
+ Class eventClass = NSClassFromString(@"UITouchesEvent");
106
+ if (!RNCEnsureUIKitGestureEnvironment()) {
107
+ _unavailableReason = @"UIApplication singleton unavailable";
108
+ } else if (_createHand == NULL || _createFinger == NULL || _setIntegerValue == NULL ||
109
+ _appendEvent == NULL || _setSenderID == NULL) {
110
+ _unavailableReason = @"IOHIDEvent symbols unavailable";
111
+ } else if (eventClass == Nil || ![eventClass instancesRespondToSelector:@selector(_init)] ||
112
+ ![eventClass instancesRespondToSelector:@selector(_addTouch:forDelayedDelivery:)] ||
113
+ ![UITouch instancesRespondToSelector:@selector(_setLocationInWindow:resetPrevious:)] ||
114
+ ![UITouch instancesRespondToSelector:@selector(_setHidEvent:)]) {
115
+ _unavailableReason = @"UITouch/UITouchesEvent synthesis selectors unavailable";
116
+ } else {
117
+ _event = [[eventClass alloc] _init];
118
+ }
119
+ }
120
+ return self;
121
+ }
122
+
123
+ - (RNCTestHIDEventRef)newHIDEvent
124
+ {
125
+ static const uint32_t kDigitizerDisplayIntegrated = (11 << 16) | 25;
126
+ static const uint32_t kIsBuiltIn = 4;
127
+ static const uint32_t kOptions = (uint32_t)-268435456;
128
+ uint64_t machTime = mach_absolute_time();
129
+ // Hand transducer, touch mask.
130
+ RNCTestHIDEventRef hand =
131
+ _createHand(kCFAllocatorDefault, machTime, 3, 0, 0, 1 << 1, 0, 0, 0, 0, 0, 0, 0, 0, 0);
132
+ _setIntegerValue(hand, kDigitizerDisplayIntegrated, 1, kOptions);
133
+ _setIntegerValue(hand, kIsBuiltIn, 1, kOptions);
134
+ _setSenderID(hand, 0x000000010000027F);
135
+ uint32_t index = 0;
136
+ for (UITouch *touch in _activeTouches) {
137
+ UITouchPhase phase = touch.phase;
138
+ // Position for moves; range and touch for contact changes.
139
+ uint32_t mask = phase == UITouchPhaseMoved ? (1 << 2) : ((1 << 0) | (1 << 1));
140
+ Boolean down = phase != UITouchPhaseEnded && phase != UITouchPhaseCancelled;
141
+ CGPoint point = [touch locationInView:_window];
142
+ RNCTestHIDEventRef finger = _createFinger(kCFAllocatorDefault, machTime, ++index, 2, mask,
143
+ point.x, point.y, 0, 0, 0, 5, 5, 1, 1, 1,
144
+ down, down, 0);
145
+ _setIntegerValue(finger, kDigitizerDisplayIntegrated, 1, kOptions);
146
+ _appendEvent(hand, finger, 0);
147
+ CFRelease(finger);
148
+ }
149
+ return hand;
150
+ }
151
+
152
+ - (void)sendChangingTouch:(UITouch *)changed phase:(UITouchPhase)phase
153
+ {
154
+ NSAssert(_event != nil, @"%@", _unavailableReason);
155
+ _timestamp += 1.0 / 60.0;
156
+ for (UITouch *touch in _activeTouches) {
157
+ [touch setPhase:touch == changed ? phase : UITouchPhaseStationary];
158
+ [touch setTimestamp:_timestamp];
159
+ }
160
+ RNCTestHIDEventRef hid = [self newHIDEvent];
161
+ for (UITouch *touch in _activeTouches) [touch _setHidEvent:hid];
162
+ [_event _setHIDEvent:hid];
163
+ if (phase == UITouchPhaseBegan) [_event _addTouch:changed forDelayedDelivery:NO];
164
+ CFRelease(hid);
165
+ [_window sendEvent:_event];
166
+ if (phase == UITouchPhaseEnded || phase == UITouchPhaseCancelled) {
167
+ [_activeTouches removeObjectIdenticalTo:changed];
168
+ if (_activeTouches.count == 0) [_event _clearTouches];
169
+ }
170
+ }
171
+
172
+ - (UITouch *)beginAt:(CGPoint)windowPoint
173
+ {
174
+ if (_activeTouches.count == 0) [_event _clearTouches];
175
+ UITouch *touch = [UITouch new];
176
+ UIView *view = [_window hitTest:windowPoint withEvent:nil];
177
+ [touch setWindow:_window];
178
+ [touch _setLocationInWindow:windowPoint resetPrevious:YES];
179
+ [touch setView:view];
180
+ if ([touch respondsToSelector:@selector(setGestureView:)]) [touch setGestureView:view];
181
+ [touch _setIsTapToClick:NO];
182
+ [touch setTapCount:1];
183
+ [_activeTouches addObject:touch];
184
+ [self sendChangingTouch:touch phase:UITouchPhaseBegan];
185
+ return touch;
186
+ }
187
+
188
+ - (void)move:(UITouch *)touch to:(CGPoint)windowPoint
189
+ {
190
+ [touch _setLocationInWindow:windowPoint resetPrevious:NO];
191
+ [self sendChangingTouch:touch phase:UITouchPhaseMoved];
192
+ }
193
+
194
+ - (void)end:(UITouch *)touch
195
+ {
196
+ [self sendChangingTouch:touch phase:UITouchPhaseEnded];
197
+ }
198
+
199
+ @end
200
+
201
+ // Private declarations access the production class compiled into the pod.
202
+ @interface RNCCollapsiblePagerContentPressCancellationGestureRecognizer : UIGestureRecognizer
203
+ @property (nonatomic, weak) UIGestureRecognizer *reactTouchHandler;
204
+ - (void)cancelPressForPan:(UIPanGestureRecognizer *)pan;
205
+ - (void)observeIncomingTouch:(UITouch *)touch;
206
+ @end
207
+
208
+ // Terminal states are reset by UIKit once it processes them. Observe requests
209
+ // while retaining the production implementation and UIKit setter.
210
+ @interface RNCPressCancellationObservedBridge : RNCCollapsiblePagerContentPressCancellationGestureRecognizer
211
+ @property (nonatomic, strong) NSMutableArray<NSNumber *> *requestedStates;
212
+ @end
213
+ @implementation RNCPressCancellationObservedBridge
214
+ - (void)setState:(UIGestureRecognizerState)state
215
+ {
216
+ if (self.requestedStates == nil) self.requestedStates = [NSMutableArray new];
217
+ [self.requestedStates addObject:@(state)];
218
+ [super setState:state];
219
+ }
220
+ @end
221
+
222
+ // These doubles supply input facts to the bridge's decision logic directly.
223
+ // The production bridge owns its state transitions. End-to-end UIKit delivery
224
+ // through the real pager is covered by the delivery suite below.
225
+ @interface RNCPressCancellationTestTouch : UITouch
226
+ @property (nonatomic, strong) UIView *testView;
227
+ @property (nonatomic, assign) UITouchPhase testPhase;
228
+ @end
229
+ @implementation RNCPressCancellationTestTouch
230
+ - (UIView *)view { return self.testView; }
231
+ - (UITouchPhase)phase { return self.testPhase; }
232
+ @end
233
+
234
+ @interface RNCPressCancellationTestEvent : UIEvent
235
+ @property (nonatomic, copy) NSSet<UITouch *> *testTouches;
236
+ @end
237
+ @implementation RNCPressCancellationTestEvent
238
+ - (NSSet<UITouch *> *)allTouches { return self.testTouches; }
239
+ @end
240
+
241
+ @interface RNCPressCancellationTestPan : UIPanGestureRecognizer
242
+ @property (nonatomic, assign) UIGestureRecognizerState testState;
243
+ @end
244
+ @implementation RNCPressCancellationTestPan
245
+ - (UIGestureRecognizerState)state { return self.testState; }
246
+ @end
247
+
248
+ @interface RNCCollapsiblePagerPressCancellationTests : XCTestCase
249
+ @property (nonatomic, strong) UIWindow *window;
250
+ @property (nonatomic, strong) UIView *host;
251
+ @property (nonatomic, strong) UIView *surface;
252
+ @property (nonatomic, strong) UIView *content;
253
+ @property (nonatomic, strong) RCTSurfaceTouchHandler *handler;
254
+ @property (nonatomic, strong) RNCPressCancellationObservedBridge *bridge;
255
+ @property (nonatomic, strong) RNCPressCancellationTestPan *pan;
256
+ @property (nonatomic, strong) RNCPressCancellationTestTouch *touch;
257
+ @end
258
+
259
+ @implementation RNCCollapsiblePagerPressCancellationTests
260
+
261
+ - (void)setUp
262
+ {
263
+ [super setUp];
264
+ XCTAssertTrue(NSThread.isMainThread);
265
+ // Without an application UIKit drops every programmatic recognizer state.
266
+ XCTAssertTrue(RNCEnsureUIKitGestureEnvironment());
267
+ // Register the recognizers with a UIKit gesture environment without taking focus.
268
+ self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
269
+ self.window.hidden = NO;
270
+ self.host = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 600)];
271
+ self.surface = [[UIView alloc] initWithFrame:self.host.bounds];
272
+ self.content = [[UIView alloc] initWithFrame:self.surface.bounds];
273
+ [self.window addSubview:self.host];
274
+ [self.host addSubview:self.surface];
275
+ [self.surface addSubview:self.content];
276
+ self.handler = [RCTSurfaceTouchHandler new];
277
+ [self.handler attachToView:self.surface];
278
+ Class bridgeClass = NSClassFromString(@"RNCCollapsiblePagerContentPressCancellationGestureRecognizer");
279
+ XCTAssertNotNil(bridgeClass);
280
+ self.bridge = [RNCPressCancellationObservedBridge new];
281
+ self.bridge.reactTouchHandler = self.handler;
282
+ [self.host addGestureRecognizer:self.bridge];
283
+ XCTAssertEqual(self.bridge.view.window, self.window);
284
+ XCTAssertEqual(self.handler.view.window, self.window);
285
+ self.pan = [RNCPressCancellationTestPan new];
286
+ self.pan.testState = UIGestureRecognizerStateBegan;
287
+ self.touch = [self touchInView:self.content phase:UITouchPhaseBegan];
288
+ }
289
+
290
+ - (void)tearDown
291
+ {
292
+ [self.host removeGestureRecognizer:self.bridge];
293
+ [self.handler detachFromView:self.surface];
294
+ self.bridge = nil;
295
+ self.handler = nil;
296
+ self.touch = nil;
297
+ self.pan = nil;
298
+ self.content = nil;
299
+ self.surface = nil;
300
+ [self.host removeFromSuperview];
301
+ self.host = nil;
302
+ self.window.hidden = YES;
303
+ self.window = nil;
304
+ [super tearDown];
305
+ }
306
+
307
+ - (RNCPressCancellationTestTouch *)touchInView:(UIView *)view phase:(UITouchPhase)phase
308
+ {
309
+ RNCPressCancellationTestTouch *touch = [RNCPressCancellationTestTouch new];
310
+ touch.testView = view;
311
+ touch.testPhase = phase;
312
+ return touch;
313
+ }
314
+
315
+ - (RNCPressCancellationTestEvent *)eventWithTouches:(NSSet<UITouch *> *)touches
316
+ {
317
+ RNCPressCancellationTestEvent *event = [RNCPressCancellationTestEvent new];
318
+ event.testTouches = touches;
319
+ return event;
320
+ }
321
+
322
+ - (void)beginSingleTouch
323
+ {
324
+ [self.bridge touchesBegan:[NSSet setWithObject:self.touch]
325
+ withEvent:[self eventWithTouches:[NSSet setWithObject:self.touch]]];
326
+ }
327
+
328
+ - (void)testPreventionTargetsExactHandlerAndUsesExternalHost
329
+ {
330
+ RCTSurfaceTouchHandler *otherHandler = [RCTSurfaceTouchHandler new];
331
+ UIPanGestureRecognizer *nativePan = [UIPanGestureRecognizer new];
332
+ XCTAssertTrue([self.bridge canPreventGestureRecognizer:self.handler]);
333
+ XCTAssertFalse([self.bridge canPreventGestureRecognizer:otherHandler]);
334
+ XCTAssertFalse([self.bridge canPreventGestureRecognizer:nativePan]);
335
+ XCTAssertTrue([self.handler canBePreventedByGestureRecognizer:self.bridge]);
336
+
337
+ [self.content addGestureRecognizer:nativePan];
338
+ XCTAssertFalse([self.handler canBePreventedByGestureRecognizer:nativePan]);
339
+ }
340
+
341
+ - (void)testOnlyBeganPanWithTrackedLiveTouchActivates
342
+ {
343
+ [self.bridge cancelPressForPan:self.pan];
344
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
345
+ [self beginSingleTouch];
346
+ for (NSNumber *state in @[@(UIGestureRecognizerStatePossible),
347
+ @(UIGestureRecognizerStateChanged),
348
+ @(UIGestureRecognizerStateFailed),
349
+ @(UIGestureRecognizerStateEnded)]) {
350
+ self.pan.testState = (UIGestureRecognizerState)state.integerValue;
351
+ [self.bridge cancelPressForPan:self.pan];
352
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
353
+ }
354
+ self.pan.testState = UIGestureRecognizerStateBegan;
355
+ [self.bridge cancelPressForPan:self.pan];
356
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
357
+ @"The production bridge must request recognition before UIKit retains Began");
358
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
359
+ }
360
+
361
+ - (void)testEndedAndCancelledOriginTouchesCannotActivate
362
+ {
363
+ [self beginSingleTouch];
364
+ for (NSNumber *phase in @[@(UITouchPhaseEnded), @(UITouchPhaseCancelled)]) {
365
+ self.touch.testPhase = (UITouchPhase)phase.integerValue;
366
+ [self.bridge cancelPressForPan:self.pan];
367
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
368
+ }
369
+ }
370
+
371
+ - (void)testSecondActiveTouchOutsidePagerButOnSameSurfaceBlocksCancellation
372
+ {
373
+ UIView *sibling = [UIView new];
374
+ [self.surface addSubview:sibling];
375
+ RNCPressCancellationTestTouch *second = [self touchInView:sibling phase:UITouchPhaseStationary];
376
+ [self beginSingleTouch];
377
+ [self.bridge touchesMoved:[NSSet setWithObject:self.touch]
378
+ withEvent:[self eventWithTouches:[NSSet setWithObjects:self.touch, second, nil]]];
379
+ [self.bridge cancelPressForPan:self.pan];
380
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
381
+ }
382
+
383
+ - (void)testOtherSurfaceAndFinishedTouchesDoNotCountAsActiveSurfaceTouches
384
+ {
385
+ UIView *otherSurface = [UIView new];
386
+ [self.host addSubview:otherSurface];
387
+ RNCPressCancellationTestTouch *other = [self touchInView:otherSurface phase:UITouchPhaseMoved];
388
+ RNCPressCancellationTestTouch *ended = [self touchInView:self.surface phase:UITouchPhaseEnded];
389
+ RNCPressCancellationTestTouch *cancelled = [self touchInView:self.content phase:UITouchPhaseCancelled];
390
+ [self.bridge touchesBegan:[NSSet setWithObject:self.touch]
391
+ withEvent:[self eventWithTouches:[NSSet setWithObjects:self.touch, other, ended, cancelled, nil]]];
392
+ [self.bridge cancelPressForPan:self.pan];
393
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
394
+ @"The production bridge must request recognition before UIKit retains Began");
395
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
396
+ }
397
+
398
+ - (void)testMultipleOriginTouchesFailWithoutRecognizing
399
+ {
400
+ RNCPressCancellationTestTouch *second = [self touchInView:self.content phase:UITouchPhaseBegan];
401
+ NSSet<UITouch *> *touches = [NSSet setWithObjects:self.touch, second, nil];
402
+ [self.bridge touchesBegan:touches withEvent:[self eventWithTouches:touches]];
403
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateFailed)]);
404
+ XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
405
+ [self.bridge cancelPressForPan:self.pan];
406
+ XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
407
+ }
408
+
409
+ - (void)testSecondTouchAfterRecognitionDoesNotFailOrEndTheTrackedGesture
410
+ {
411
+ [self beginSingleTouch];
412
+ [self.bridge cancelPressForPan:self.pan];
413
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
414
+ @"The production bridge must request recognition before UIKit retains Began");
415
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
416
+ RNCPressCancellationTestTouch *second = [self touchInView:self.content phase:UITouchPhaseBegan];
417
+ RNCPressCancellationTestEvent *event =
418
+ [self eventWithTouches:[NSSet setWithObjects:self.touch, second, nil]];
419
+ NSUInteger requestCount = self.bridge.requestedStates.count;
420
+ XCTAssertNoThrow([self.bridge touchesBegan:[NSSet setWithObject:second] withEvent:event]);
421
+ XCTAssertEqual(self.bridge.requestedStates.count, requestCount);
422
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
423
+ second.testPhase = UITouchPhaseEnded;
424
+ [self.bridge touchesEnded:[NSSet setWithObject:second] withEvent:event];
425
+ XCTAssertEqual(self.bridge.requestedStates.count, requestCount);
426
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
427
+ self.touch.testPhase = UITouchPhaseEnded;
428
+ [self.bridge touchesEnded:[NSSet setWithObject:self.touch] withEvent:event];
429
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateEnded)]);
430
+ XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
431
+ }
432
+
433
+ - (void)testSecondTouchCancellationDoesNotCancelTheTrackedGesture
434
+ {
435
+ [self beginSingleTouch];
436
+ [self.bridge cancelPressForPan:self.pan];
437
+ RNCPressCancellationTestTouch *second = [self touchInView:self.content phase:UITouchPhaseBegan];
438
+ RNCPressCancellationTestEvent *event =
439
+ [self eventWithTouches:[NSSet setWithObjects:self.touch, second, nil]];
440
+ [self.bridge touchesBegan:[NSSet setWithObject:second] withEvent:event];
441
+ second.testPhase = UITouchPhaseCancelled;
442
+ [self.bridge touchesCancelled:[NSSet setWithObject:second] withEvent:event];
443
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
444
+ self.touch.testPhase = UITouchPhaseCancelled;
445
+ [self.bridge touchesCancelled:[NSSet setWithObject:self.touch] withEvent:event];
446
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateCancelled)]);
447
+ XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
448
+ }
449
+
450
+ - (void)testRejectedSameSurfaceTouchRemainsLatchedUntilReset
451
+ {
452
+ UIView *sibling = [UIView new];
453
+ [self.surface addSubview:sibling];
454
+ RNCPressCancellationTestTouch *second = [self touchInView:sibling phase:UITouchPhaseBegan];
455
+ [self beginSingleTouch];
456
+ // The pager delegate observes this touch even when it rejects delivery to the bridge.
457
+ [self.bridge observeIncomingTouch:second];
458
+ second.testPhase = UITouchPhaseEnded;
459
+ [self.bridge touchesMoved:[NSSet setWithObject:self.touch]
460
+ withEvent:[self eventWithTouches:[NSSet setWithObject:self.touch]]];
461
+ [self.bridge cancelPressForPan:self.pan];
462
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
463
+ [self.bridge reset];
464
+ [self beginSingleTouch];
465
+ [self.bridge cancelPressForPan:self.pan];
466
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
467
+ @"The production bridge must request recognition before UIKit retains Began");
468
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
469
+ }
470
+
471
+ - (void)testObservedOtherSurfaceTouchDoesNotLatchCurrentSurface
472
+ {
473
+ UIView *otherSurface = [UIView new];
474
+ [self.host addSubview:otherSurface];
475
+ RNCPressCancellationTestTouch *second = [self touchInView:otherSurface phase:UITouchPhaseBegan];
476
+ [self beginSingleTouch];
477
+ [self.bridge observeIncomingTouch:second];
478
+ [self.bridge cancelPressForPan:self.pan];
479
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
480
+ @"The production bridge must request recognition before UIKit retains Began");
481
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
482
+ }
483
+
484
+ - (void)testHostMismatchDoesNotCancel
485
+ {
486
+ [self.host removeGestureRecognizer:self.bridge];
487
+ [self.surface addGestureRecognizer:self.bridge];
488
+ [self beginSingleTouch];
489
+ [self.bridge cancelPressForPan:self.pan];
490
+ XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
491
+ [self.surface removeGestureRecognizer:self.bridge];
492
+ }
493
+
494
+ - (void)testResetDropsPendingTouchAndAllowsANewSequence
495
+ {
496
+ [self beginSingleTouch];
497
+ [self.bridge reset];
498
+ [self.bridge cancelPressForPan:self.pan];
499
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
500
+ [self beginSingleTouch];
501
+ [self.bridge cancelPressForPan:self.pan];
502
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
503
+ @"The production bridge must request recognition before UIKit retains Began");
504
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
505
+ }
506
+
507
+ - (void)testRecognizedSequenceEndsAndReleasesEvent
508
+ {
509
+ __weak UIEvent *weakEvent;
510
+ @autoreleasepool {
511
+ RNCPressCancellationTestEvent *event = [self eventWithTouches:[NSSet setWithObject:self.touch]];
512
+ weakEvent = event;
513
+ [self.bridge touchesBegan:[NSSet setWithObject:self.touch] withEvent:event];
514
+ [self.bridge cancelPressForPan:self.pan];
515
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
516
+ @"The production bridge must request recognition before UIKit retains Began");
517
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
518
+ self.touch.testPhase = UITouchPhaseEnded;
519
+ [self.bridge touchesEnded:[NSSet setWithObject:self.touch] withEvent:event];
520
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateEnded)]);
521
+ XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
522
+ }
523
+ XCTAssertNil(weakEvent);
524
+ }
525
+
526
+ - (void)testUnrecognizedCancellationFailsAndReleasesEvent
527
+ {
528
+ __weak UIEvent *weakEvent;
529
+ @autoreleasepool {
530
+ RNCPressCancellationTestEvent *event = [self eventWithTouches:[NSSet setWithObject:self.touch]];
531
+ weakEvent = event;
532
+ [self.bridge touchesBegan:[NSSet setWithObject:self.touch] withEvent:event];
533
+ self.touch.testPhase = UITouchPhaseCancelled;
534
+ [self.bridge touchesCancelled:[NSSet setWithObject:self.touch] withEvent:event];
535
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateFailed)]);
536
+ XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
537
+ }
538
+ XCTAssertNil(weakEvent);
539
+ }
540
+
541
+ - (void)testRecognizedCancellationTerminatesTheBridge
542
+ {
543
+ [self beginSingleTouch];
544
+ [self.bridge cancelPressForPan:self.pan];
545
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateBegan)],
546
+ @"The production bridge must request recognition before UIKit retains Began");
547
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStateBegan);
548
+ self.touch.testPhase = UITouchPhaseCancelled;
549
+ [self.bridge touchesCancelled:[NSSet setWithObject:self.touch]
550
+ withEvent:[self eventWithTouches:[NSSet setWithObject:self.touch]]];
551
+ XCTAssertTrue([self.bridge.requestedStates containsObject:@(UIGestureRecognizerStateCancelled)]);
552
+ XCTAssertNotEqual(self.bridge.state, UIGestureRecognizerStateBegan);
553
+ }
554
+
555
+ - (void)testHandlerBindingIsWeak
556
+ {
557
+ __weak UIGestureRecognizer *weakHandler;
558
+ @autoreleasepool {
559
+ UIGestureRecognizer *handler = [UIGestureRecognizer new];
560
+ weakHandler = handler;
561
+ self.bridge.reactTouchHandler = handler;
562
+ XCTAssertTrue([self.bridge canPreventGestureRecognizer:handler]);
563
+ XCTAssertFalse([self.bridge canPreventGestureRecognizer:self.handler]);
564
+ }
565
+ XCTAssertNil(weakHandler);
566
+ XCTAssertNil(self.bridge.reactTouchHandler);
567
+ [self beginSingleTouch];
568
+ [self.bridge cancelPressForPan:self.pan];
569
+ XCTAssertEqual(self.bridge.state, UIGestureRecognizerStatePossible);
570
+ }
571
+
572
+ @end
573
+
574
+ #pragma mark - Real UIKit delivery through the production pager
575
+
576
+ // Records what RN's Fabric touch handler observes. Its reset dispatches
577
+ // touchCancel for every still-registered touch, which is how a Pressable loses
578
+ // its press when an external recognizer prevents the handler.
579
+ @interface RNCRecordingSurfaceTouchHandler : RCTSurfaceTouchHandler
580
+ @property (nonatomic, strong) NSMutableArray<NSString *> *events;
581
+ @property (nonatomic, assign) NSInteger registeredTouches;
582
+ @end
583
+
584
+ @implementation RNCRecordingSurfaceTouchHandler
585
+ - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
586
+ {
587
+ [super touchesBegan:touches withEvent:event];
588
+ self.registeredTouches += touches.count;
589
+ [self.events addObject:@"began"];
590
+ }
591
+ - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
592
+ {
593
+ [super touchesMoved:touches withEvent:event];
594
+ if (![self.events.lastObject isEqualToString:@"moved"]) [self.events addObject:@"moved"];
595
+ }
596
+ - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
597
+ {
598
+ [super touchesEnded:touches withEvent:event];
599
+ self.registeredTouches -= touches.count;
600
+ [self.events addObject:@"ended"];
601
+ }
602
+ - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
603
+ {
604
+ [super touchesCancelled:touches withEvent:event];
605
+ self.registeredTouches -= touches.count;
606
+ [self.events addObject:@"cancelled"];
607
+ }
608
+ - (void)reset
609
+ {
610
+ NSInteger pending = self.registeredTouches;
611
+ [super reset];
612
+ self.registeredTouches = 0;
613
+ if (pending > 0) [self.events addObject:@"reset-cancelled"];
614
+ }
615
+ @end
616
+
617
+ @interface RNCCollapsiblePagerPressCancellationDeliveryTests : XCTestCase
618
+ @property (nonatomic, strong) UIWindow *window;
619
+ @property (nonatomic, strong) UIView *surface;
620
+ @property (nonatomic, strong) RNCRecordingSurfaceTouchHandler *handler;
621
+ @property (nonatomic, strong) RNCCollapsiblePagerViewComponentView *pager;
622
+ @property (nonatomic, strong) RNCSynthesizedTouchDriver *driver;
623
+ @property (nonatomic, strong) UIView *firstPage;
624
+ @end
625
+
626
+ @implementation RNCCollapsiblePagerPressCancellationDeliveryTests
627
+
628
+ - (void)setUp
629
+ {
630
+ [super setUp];
631
+ self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 390, 800)];
632
+ self.window.hidden = NO;
633
+ UIView *host = [[UIView alloc] initWithFrame:self.window.bounds];
634
+ self.surface = [[UIView alloc] initWithFrame:host.bounds];
635
+ [self.window addSubview:host];
636
+ [host addSubview:self.surface];
637
+ self.handler = [RNCRecordingSurfaceTouchHandler new];
638
+ self.handler.events = [NSMutableArray new];
639
+ [self.handler attachToView:self.surface];
640
+
641
+ self.pager = [RNCCollapsiblePagerViewComponentView new];
642
+ auto props = std::make_shared<RNCCollapsiblePagerViewProps>();
643
+ props->nativeSmoothHeaderScrollEnabled = true;
644
+ props->headerHeight = 120;
645
+ props->stickyHeaderHeight = 44;
646
+ [self.pager updateProps:props oldProps:nullptr];
647
+ self.pager.frame = self.surface.bounds;
648
+ [self.surface addSubview:self.pager];
649
+ NSArray<UIView<RCTComponentViewProtocol> *> *children = @[
650
+ [RCTViewComponentView new], // header
651
+ [RCTViewComponentView new], // sticky header
652
+ [RCTViewComponentView new], // page 0
653
+ [RCTViewComponentView new], // page 1
654
+ ];
655
+ children[0].frame = CGRectMake(0, 0, 390, 120);
656
+ children[1].frame = CGRectMake(0, 0, 390, 44);
657
+ [children enumerateObjectsUsingBlock:^(UIView<RCTComponentViewProtocol> *child,
658
+ NSUInteger index,
659
+ BOOL *stop) {
660
+ [self.pager mountChildComponentView:child index:(NSInteger)index];
661
+ }];
662
+ self.firstPage = children[2];
663
+ [self.window layoutIfNeeded];
664
+ [self drainMainQueue];
665
+ self.firstPage.frame = self.firstPage.superview.bounds;
666
+ self.driver = [[RNCSynthesizedTouchDriver alloc] initWithWindow:self.window];
667
+ }
668
+
669
+ - (void)tearDown
670
+ {
671
+ [self.pager removeFromSuperview];
672
+ [self.handler detachFromView:self.surface];
673
+ self.window.hidden = YES;
674
+ self.driver = nil;
675
+ self.firstPage = nil;
676
+ self.pager = nil;
677
+ self.handler = nil;
678
+ self.surface = nil;
679
+ self.window = nil;
680
+ [super tearDown];
681
+ }
682
+
683
+ - (void)drainMainQueue
684
+ {
685
+ XCTestExpectation *drained = [self expectationWithDescription:@"main queue drained"];
686
+ dispatch_async(dispatch_get_main_queue(), ^{ [drained fulfill]; });
687
+ [self waitForExpectations:@[drained] timeout:2];
688
+ }
689
+
690
+ - (void)assertFixtureIsReadyForDelivery
691
+ {
692
+ XCTAssertNil(self.driver.unavailableReason);
693
+ UIGestureRecognizer *bridge = [self.pager valueForKey:@"contentPressCancellationGesture"];
694
+ UIScrollView *pagerScrollView = [self.pager valueForKey:@"pagerScrollView"];
695
+ XCTAssertNotNil(bridge);
696
+ XCTAssertEqual([bridge valueForKey:@"reactTouchHandler"], self.handler);
697
+ XCTAssertEqual((id)pagerScrollView.delegate, self.pager);
698
+ XCTAssertGreaterThan(pagerScrollView.contentSize.width, CGRectGetWidth(pagerScrollView.bounds));
699
+ // The touch must start on React page content, outside the shared header.
700
+ CGPoint point = [self pressPoint];
701
+ UIView *hit = [self.window hitTest:point withEvent:nil];
702
+ XCTAssertTrue(hit == self.firstPage || [hit isDescendantOfView:self.firstPage],
703
+ @"hit view %@", hit);
704
+ }
705
+
706
+ - (CGPoint)pressPoint
707
+ {
708
+ return CGPointMake(200, 600);
709
+ }
710
+
711
+ - (void)testHorizontalPagerDragCancelsReactPressBeforeFingerLifts
712
+ {
713
+ [self assertFixtureIsReadyForDelivery];
714
+ UIScrollView *pagerScrollView = [self.pager valueForKey:@"pagerScrollView"];
715
+ CGPoint point = [self pressPoint];
716
+ UITouch *touch = [self.driver beginAt:point];
717
+ XCTAssertEqualObjects(self.handler.events, @[@"began"]);
718
+ XCTAssertEqual(self.handler.registeredTouches, 1);
719
+
720
+ for (NSInteger step = 1; step <= 8 && !pagerScrollView.isDragging; ++step) {
721
+ [self.driver move:touch to:CGPointMake(point.x - 12 * step, point.y)];
722
+ }
723
+ XCTAssertTrue(pagerScrollView.isDragging, @"the real pager pan must begin");
724
+ // Finger is still down: RN must already have cancelled its responder.
725
+ XCTAssertEqual(self.handler.registeredTouches, 0);
726
+ XCTAssertTrue([self.handler.events containsObject:@"reset-cancelled"] ||
727
+ [self.handler.events containsObject:@"cancelled"],
728
+ @"handler events %@", self.handler.events);
729
+ XCTAssertFalse([self.handler.events containsObject:@"ended"]);
730
+ NSUInteger eventsBeforeLift = self.handler.events.count;
731
+
732
+ [self.driver end:touch];
733
+ // UIKit must not deliver the lift to the prevented handler as a press end.
734
+ XCTAssertFalse([self.handler.events containsObject:@"ended"], @"events %@", self.handler.events);
735
+ XCTAssertEqual(self.handler.events.count, eventsBeforeLift);
736
+ }
737
+
738
+ - (void)testStationaryTapKeepsReactPress
739
+ {
740
+ [self assertFixtureIsReadyForDelivery];
741
+ UITouch *touch = [self.driver beginAt:[self pressPoint]];
742
+ [self.driver end:touch];
743
+ XCTAssertEqualObjects(self.handler.events, (@[@"began", @"ended"]));
744
+ XCTAssertEqual(self.handler.registeredTouches, 0);
745
+ }
746
+
747
+ - (void)testVerticalPageDragCancelsReactPressBeforeFingerLifts
748
+ {
749
+ [self assertFixtureIsReadyForDelivery];
750
+ CGPoint point = [self pressPoint];
751
+ UITouch *touch = [self.driver beginAt:point];
752
+ XCTAssertEqual(self.handler.registeredTouches, 1);
753
+
754
+ [self.driver move:touch to:CGPointMake(point.x, point.y - 12)];
755
+ XCTAssertEqual(self.handler.registeredTouches, 0);
756
+ XCTAssertTrue([self.handler.events containsObject:@"reset-cancelled"] ||
757
+ [self.handler.events containsObject:@"cancelled"],
758
+ @"handler events %@", self.handler.events);
759
+ XCTAssertFalse([self.handler.events containsObject:@"ended"]);
760
+
761
+ [self.driver end:touch];
762
+ XCTAssertFalse([self.handler.events containsObject:@"ended"], @"events %@", self.handler.events);
763
+ }
764
+
765
+ - (void)testSmallVerticalMovementKeepsReactPress
766
+ {
767
+ [self assertFixtureIsReadyForDelivery];
768
+ CGPoint point = [self pressPoint];
769
+ UITouch *touch = [self.driver beginAt:point];
770
+ [self.driver move:touch to:CGPointMake(point.x, point.y - 8)];
771
+ XCTAssertEqual(self.handler.registeredTouches, 1);
772
+ [self.driver end:touch];
773
+ XCTAssertEqualObjects(self.handler.events, (@[@"began", @"moved", @"ended"]));
774
+ }
775
+
776
+ - (void)testSmallMovementBelowPanThresholdKeepsReactPress
777
+ {
778
+ [self assertFixtureIsReadyForDelivery];
779
+ UIScrollView *pagerScrollView = [self.pager valueForKey:@"pagerScrollView"];
780
+ CGPoint point = [self pressPoint];
781
+ UITouch *touch = [self.driver beginAt:point];
782
+ [self.driver move:touch to:CGPointMake(point.x - 2, point.y)];
783
+ XCTAssertFalse(pagerScrollView.isDragging);
784
+ [self.driver end:touch];
785
+ XCTAssertEqualObjects(self.handler.events, (@[@"began", @"moved", @"ended"]));
786
+ }
787
+
788
+ @end