@onekeyfe/react-native-pager-view 6.7.0-alpha.428 → 6.7.0-alpha.432

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.
@@ -12,6 +12,19 @@ using namespace facebook::react;
12
12
  - (void)updateTopScrollEdgeForScrollView:(UIScrollView *)scrollView;
13
13
  - (void)updateScrollEdgeContentMasks;
14
14
  - (void)layoutSharedHeaders;
15
+ - (void)adoptCurrentScrollEdgeForProgrammaticTransition:(BOOL)allowExternalOverlay;
16
+ - (BOOL)resolveExternalTopOverlay;
17
+ - (void)beginProgrammaticScrollEdgesToIndex:(NSInteger)index allowExternalOverlay:(BOOL)allowExternalOverlay;
18
+ - (void)outerPagerDidUpdateOverlayOwner:(BOOL)active transitioning:(BOOL)transitioning;
19
+ - (void)restorePinnedStickyHeader;
20
+ - (void)finishProgrammaticScrollEdges;
21
+ - (void)clearProgrammaticScrollEdges;
22
+ - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;
23
+ - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
24
+ - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
25
+ - (void)goTo:(NSInteger)index animated:(BOOL)animated;
26
+ - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView;
27
+ - (void)emitPageSelected:(NSInteger)position;
15
28
  @end
16
29
 
17
30
  @interface UIView (NativeTabReservationTesting)
@@ -33,6 +46,19 @@ using namespace facebook::react;
33
46
  - (void)setLeftToRight:(BOOL)leftToRight;
34
47
  @end
35
48
 
49
+ @interface RNCCollapsiblePagerResizeSelectionSpy : RNCCollapsiblePagerViewComponentView
50
+ @property (nonatomic, strong) NSMutableArray<NSNumber *> *selectedIndexes;
51
+ @end
52
+
53
+ @implementation RNCCollapsiblePagerResizeSelectionSpy
54
+ - (void)emitPageSelected:(NSInteger)position
55
+ {
56
+ if (_selectedIndexes == nil) _selectedIndexes = [NSMutableArray new];
57
+ [_selectedIndexes addObject:@(position)];
58
+ [super emitPageSelected:position];
59
+ }
60
+ @end
61
+
36
62
  @interface RNCCollapsiblePagerHitTestTests : XCTestCase
37
63
  @property (nonatomic, strong) UIView *root;
38
64
  @property (nonatomic, strong) UIButton *toolbar;
@@ -390,6 +416,736 @@ using namespace facebook::react;
390
416
  }
391
417
  }
392
418
 
419
+ - (UIScrollView *)preparePinnedLocalHardSource
420
+ {
421
+ UIWindow *window = [[UIWindow alloc] initWithFrame:self.root.bounds];
422
+ [window addSubview:self.root];
423
+ window.hidden = NO;
424
+ [self addTeardownBlock:^{
425
+ [self.pager prepareForRecycle];
426
+ window.hidden = YES;
427
+ }];
428
+ UIView *container = [self.pager valueForKey:@"containerView"];
429
+ container.frame = self.pager.bounds;
430
+ UIViewController *page = [UIViewController new];
431
+ page.view = [[UIView alloc] initWithFrame:container.bounds];
432
+ UIScrollView *source = [[UIScrollView alloc] initWithFrame:page.view.bounds];
433
+ source.contentSize = CGSizeMake(320, 1200);
434
+ source.topEdgeEffect.style = UIScrollEdgeEffectStyle.softStyle;
435
+ [page.view addSubview:source];
436
+ [container insertSubview:page.view atIndex:0];
437
+ [[self.pager valueForKey:@"pageControllers"] addObject:page];
438
+ [self.pager setValue:source forKey:@"observedScrollView"];
439
+ [self.pager setValue:source forKey:@"sharedHeaderScrollView"];
440
+ [self.pager setValue:@YES forKey:@"directNativePagerEnabled"];
441
+ [self.pager setValue:@YES forKey:@"topScrollEdgeEnabled"];
442
+ [self.pager setValue:@(static_cast<NSInteger>(RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard))
443
+ forKey:@"topScrollEdgeEffect"];
444
+ UIView *overlayHost = [self.pager valueForKey:@"topOverlayHostView"];
445
+ overlayHost.frame = CGRectMake(0, 0, 320, 50);
446
+ overlayHost.hidden = NO;
447
+ UIView *overlay = [[UIView alloc] initWithFrame:overlayHost.bounds];
448
+ [overlayHost addSubview:overlay];
449
+ [self.pager setValue:overlay forKey:@"topOverlayView"];
450
+ [self.pager setValue:@50 forKey:@"topOverlayHeight"];
451
+ [self.pager setValue:@100 forKey:@"headerHeight"];
452
+ [self.pager setValue:@44 forKey:@"stickyHeaderHeight"];
453
+ [self.pager setValue:@100 forKey:@"headerOffset"];
454
+ UIView *pinnedHost = [self.pager valueForKey:@"pinnedStickyHostView"];
455
+ pinnedHost.frame = CGRectMake(0, 50, 320, 44);
456
+ pinnedHost.hidden = NO;
457
+ [source addSubview:pinnedHost];
458
+ [pinnedHost addSubview:self.sticky];
459
+ [self.pager setValue:self.sticky forKey:@"stickyHeaderView"];
460
+ [self.pager setValue:@YES forKey:@"stickyHeaderIsPinned"];
461
+ [self.pager updateTopScrollEdgeForScrollView:source];
462
+ return source;
463
+ }
464
+
465
+ - (UIScrollView *)prepareDirectDragWithSource:(UIScrollView **)sourceOut
466
+ {
467
+ UIScrollView *source = [self preparePinnedLocalHardSource];
468
+ UIView *container = [self.pager valueForKey:@"containerView"];
469
+ UIScrollView *horizontal = [[UIScrollView alloc] initWithFrame:container.bounds];
470
+ horizontal.contentSize = CGSizeMake(960, CGRectGetHeight(container.bounds));
471
+ horizontal.pagingEnabled = YES;
472
+ horizontal.delegate = (id<UIScrollViewDelegate>)self.pager;
473
+ [container insertSubview:horizontal atIndex:0];
474
+ NSMutableArray<UIViewController *> *pages = [self.pager valueForKey:@"pageControllers"];
475
+ UIViewController *sourcePage = pages.firstObject;
476
+ for (NSInteger index = 0; index < 3; index++) {
477
+ UIViewController *page = index == 1 ? sourcePage : [UIViewController new];
478
+ if (index != 1) {
479
+ page.view = [[UIView alloc] initWithFrame:container.bounds];
480
+ UIScrollView *list = [[UIScrollView alloc] initWithFrame:page.view.bounds];
481
+ list.contentSize = CGSizeMake(320, 1200);
482
+ list.topEdgeEffect.style = UIScrollEdgeEffectStyle.softStyle;
483
+ [page.view addSubview:list];
484
+ }
485
+ page.view.frame = CGRectMake(index * 320, 0, 320, CGRectGetHeight(container.bounds));
486
+ [horizontal addSubview:page.view];
487
+ if (index == 0) [pages insertObject:page atIndex:0];
488
+ if (index == 2) [pages addObject:page];
489
+ }
490
+ horizontal.contentOffset = CGPointMake(320, 0);
491
+ [self.pager setValue:horizontal forKey:@"pagerScrollView"];
492
+ [self.pager setValue:@1 forKey:@"currentIndex"];
493
+ [self.pager setValue:@1 forKey:@"destinationIndex"];
494
+ [self.pager setValue:@"ltr" forKey:@"layoutDirection"];
495
+ [self.pager setValue:@YES forKey:@"hasAppliedInitialPage"];
496
+ if (sourceOut != nil) *sourceOut = source;
497
+ return horizontal;
498
+ }
499
+
500
+ - (UIScrollView *)prepareExternalDirectDragWithSource:(UIScrollView **)sourceOut
501
+ overlay:(UIView **)overlayOut
502
+ {
503
+ UIScrollView *source = nil;
504
+ UIScrollView *horizontal = [self prepareDirectDragWithSource:&source];
505
+ [self.pager updateTopScrollEdgeForScrollView:nil];
506
+ UIView *local = [self.pager valueForKey:@"topOverlayView"];
507
+ [local removeFromSuperview];
508
+ [self.pager setValue:nil forKey:@"topOverlayView"];
509
+ [self.pager setValue:@0 forKey:@"topOverlayHeight"];
510
+ UIView *overlay = [[UIView alloc] initWithFrame:CGRectMake(0, 100, 320, 50)];
511
+ overlay.tag = 8675309;
512
+ [self.root addSubview:overlay];
513
+ auto props = std::make_shared<RNCCollapsiblePagerViewProps>();
514
+ props->nativeSmoothHeaderScrollEnabled = true;
515
+ props->topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard;
516
+ props->externalTopOverlayTag = overlay.tag;
517
+ [self.pager updateProps:props oldProps:nullptr];
518
+ [self.pager resolveExternalTopOverlay];
519
+ XCTAssertEqual([self.pager valueForKey:@"externalTopOverlayView"], overlay);
520
+ XCTAssertEqualWithAccuracy([[self.pager valueForKey:@"externalTopOverlayHeight"] doubleValue], 50, 0.001);
521
+ XCTAssertEqual(overlay.window, self.pager.window);
522
+ XCTAssertFalse([overlay isDescendantOfView:self.pager]);
523
+ UIView *proxy = [self.pager valueForKey:@"topOverlayHostView"];
524
+ XCTAssertEqual(proxy.window, self.pager.window);
525
+ XCTAssertFalse(proxy.hidden);
526
+ XCTAssertFalse(proxy.userInteractionEnabled);
527
+ [self.pager updateTopScrollEdgeForScrollView:source];
528
+ XCTAssertEqual([[self.pager valueForKey:@"pageControllers"] count], 3);
529
+ XCTAssertEqual([[self.pager valueForKey:@"currentIndex"] integerValue], 1);
530
+ XCTAssertEqualWithAccuracy(horizontal.contentOffset.x, 320, 0.001);
531
+ XCTAssertTrue([[self.pager valueForKey:@"hasAppliedInitialPage"] boolValue]);
532
+ XCTAssertTrue([[self.pager valueForKey:@"directNativePagerEnabled"] boolValue]);
533
+ XCTAssertEqualWithAccuracy([[self.pager valueForKey:@"headerOffset"] doubleValue], 100, 0.001);
534
+ XCTAssertTrue([[self.pager valueForKey:@"stickyHeaderIsPinned"] boolValue]);
535
+ XCTAssertNotNil([self.pager valueForKey:@"stickyEdgeInteraction"]);
536
+ if (sourceOut != nil) *sourceOut = source;
537
+ if (overlayOut != nil) *overlayOut = overlay;
538
+ return horizontal;
539
+ }
540
+
541
+ - (UIScrollView *)prepareNativeResizeCommandPager
542
+ {
543
+ [self.pager removeFromSuperview];
544
+ self.pager = [RNCCollapsiblePagerResizeSelectionSpy new];
545
+ [self setPointerEvents:PointerEventsMode::Auto];
546
+ self.pager.frame = CGRectMake(0, 100, 320, 500);
547
+ [self.root addSubview:self.pager];
548
+ UIScrollView *horizontal = [self prepareDirectDragWithSource:NULL];
549
+ XCTAssertEqual([[self.pager valueForKey:@"currentIndex"] integerValue], 1);
550
+ XCTAssertEqualWithAccuracy(CGRectGetWidth(horizontal.bounds), 320, 0.001);
551
+ XCTAssertEqualWithAccuracy(horizontal.contentOffset.x, 320, 0.001);
552
+ XCTAssertTrue(horizontal.scrollEnabled);
553
+ return horizontal;
554
+ }
555
+
556
+ - (void)waitForNativeResizeCommandsToSettle
557
+ {
558
+ NSPredicate *settled = [NSPredicate predicateWithBlock:^BOOL(id object, NSDictionary *bindings) {
559
+ return ![[self.pager valueForKey:@"transitioning"] boolValue] &&
560
+ [self.pager valueForKey:@"pendingNativePagerScrollCompletion"] == nil &&
561
+ [[self.pager valueForKey:@"pendingGoToIndex"] integerValue] == -1;
562
+ }];
563
+ XCTNSPredicateExpectation *expectation = [[XCTNSPredicateExpectation alloc]
564
+ initWithPredicate:settled object:self.pager];
565
+ [self waitForExpectations:@[expectation] timeout:3];
566
+ }
567
+
568
+ - (void)testNativeAnimatedCommandResizeRebasesAndDrainsQueueIgnoringLateEnd
569
+ {
570
+ if (@available(iOS 27.0, *)) {
571
+ UIScrollView *horizontal = [self prepareNativeResizeCommandPager];
572
+ RNCCollapsiblePagerResizeSelectionSpy *spy = (id)self.pager;
573
+ [self.pager goTo:2 animated:YES];
574
+ id originalPending = [self.pager valueForKey:@"pendingNativePagerScrollCompletion"];
575
+ XCTAssertNotNil(originalPending);
576
+ XCTAssertFalse(horizontal.scrollEnabled);
577
+ XCTAssertEqualWithAccuracy([[self.pager valueForKey:@"pendingNativePagerScrollTarget"] doubleValue], 640, 0.001);
578
+ self.pager.frame = CGRectMake(0, 100, 320, 480);
579
+ [self.pager layoutSubviews];
580
+ XCTAssertEqual([self.pager valueForKey:@"pendingNativePagerScrollCompletion"], originalPending);
581
+ XCTAssertEqual(spy.selectedIndexes.count, 0);
582
+ [self.pager goTo:0 animated:YES];
583
+ XCTAssertEqual([[self.pager valueForKey:@"pendingGoToIndex"] integerValue], 0);
584
+ self.pager.frame = CGRectMake(0, 100, 320.4, 480);
585
+ [self.pager layoutSubviews];
586
+ XCTAssertNil([self.pager valueForKey:@"pendingNativePagerScrollCompletion"]);
587
+ XCTAssertEqualWithAccuracy(horizontal.contentOffset.x, 640.8, 1.0 / horizontal.window.screen.scale);
588
+ XCTAssertGreaterThan(horizontal.contentOffset.x, 640.5);
589
+ XCTAssertEqual([[self.pager valueForKey:@"currentIndex"] integerValue], 2);
590
+ XCTAssertTrue(horizontal.scrollEnabled);
591
+ XCTAssertEqualObjects(spy.selectedIndexes, (@[@2]));
592
+ [self.pager scrollViewDidEndScrollingAnimation:horizontal];
593
+ XCTAssertEqualObjects(spy.selectedIndexes, (@[@2]));
594
+ XCTestExpectation *queued = [self expectationWithDescription:@"queued native command starts"];
595
+ dispatch_async(dispatch_get_main_queue(), ^{
596
+ id pending = [self.pager valueForKey:@"pendingNativePagerScrollCompletion"];
597
+ XCTAssertNotNil(pending);
598
+ XCTAssertNotEqual(pending, originalPending);
599
+ XCTAssertEqualWithAccuracy([[self.pager valueForKey:@"pendingNativePagerScrollTarget"] doubleValue], 0, 0.001);
600
+ XCTAssertGreaterThan(fabs(horizontal.contentOffset.x), 0.5);
601
+ [self.pager scrollViewDidEndScrollingAnimation:horizontal];
602
+ XCTAssertEqual([self.pager valueForKey:@"pendingNativePagerScrollCompletion"], pending);
603
+ XCTAssertEqualObjects(spy.selectedIndexes, (@[@2]));
604
+ [queued fulfill];
605
+ });
606
+ [self waitForExpectations:@[queued] timeout:2];
607
+ // This hostless fixture does not drive the natural scroll animation.
608
+ // Exercise the controlled UIKit end boundary after checking stale-end rejection.
609
+ XCTAssertNotNil([self.pager valueForKey:@"pendingNativePagerScrollCompletion"]);
610
+ [horizontal setContentOffset:CGPointZero animated:NO];
611
+ [self.pager scrollViewDidEndScrollingAnimation:horizontal];
612
+ [self waitForNativeResizeCommandsToSettle];
613
+ XCTAssertEqualWithAccuracy(horizontal.contentOffset.x, 0, 0.001);
614
+ XCTAssertEqual([[self.pager valueForKey:@"currentIndex"] integerValue], 0);
615
+ XCTAssertTrue(horizontal.scrollEnabled);
616
+ XCTAssertEqualObjects(spy.selectedIndexes, (@[@2, @0]));
617
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
618
+ } else {
619
+ XCTSkip(@"Native local Hard animation requires iOS 27 or newer");
620
+ }
621
+ }
622
+
623
+ - (void)testNativeAnimatedCommandResizeRebasesNonzeroRTLTarget
624
+ {
625
+ if (@available(iOS 27.0, *)) {
626
+ UIScrollView *horizontal = [self prepareNativeResizeCommandPager];
627
+ RNCCollapsiblePagerResizeSelectionSpy *spy = (id)self.pager;
628
+ [self.pager setValue:@"rtl" forKey:@"layoutDirection"];
629
+ [self.pager goTo:0 animated:YES];
630
+ XCTAssertNotNil([self.pager valueForKey:@"pendingNativePagerScrollCompletion"]);
631
+ XCTAssertEqualWithAccuracy([[self.pager valueForKey:@"pendingNativePagerScrollTarget"] doubleValue], 640, 0.001);
632
+ self.pager.frame = CGRectMake(0, 100, 400, 500);
633
+ [self.pager layoutSubviews];
634
+ XCTAssertNil([self.pager valueForKey:@"pendingNativePagerScrollCompletion"]);
635
+ XCTAssertEqualWithAccuracy(horizontal.contentOffset.x, 800, 0.001);
636
+ XCTAssertEqual([[self.pager valueForKey:@"currentIndex"] integerValue], 0);
637
+ [self.pager scrollViewDidEndScrollingAnimation:horizontal];
638
+ [self waitForNativeResizeCommandsToSettle];
639
+ XCTAssertEqualObjects(spy.selectedIndexes, (@[@0]));
640
+ XCTAssertTrue(horizontal.scrollEnabled);
641
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
642
+ } else {
643
+ XCTSkip(@"Native local Hard animation requires iOS 27 or newer");
644
+ }
645
+ }
646
+
647
+ - (void)testNativeAnimatedCommandZeroWidthCompletesQueueAndRealignsOnRestore
648
+ {
649
+ if (@available(iOS 27.0, *)) {
650
+ UIScrollView *horizontal = [self prepareNativeResizeCommandPager];
651
+ RNCCollapsiblePagerResizeSelectionSpy *spy = (id)self.pager;
652
+ [self.pager goTo:0 animated:YES];
653
+ XCTAssertNotNil([self.pager valueForKey:@"pendingNativePagerScrollCompletion"]);
654
+ [self.pager goTo:2 animated:YES];
655
+ self.pager.frame = CGRectMake(0, 100, 0, 500);
656
+ [self.pager layoutSubviews];
657
+ XCTAssertNil([self.pager valueForKey:@"pendingNativePagerScrollCompletion"]);
658
+ XCTAssertEqual([[self.pager valueForKey:@"currentIndex"] integerValue], 0);
659
+ XCTAssertTrue(horizontal.scrollEnabled);
660
+ [self.pager scrollViewDidEndScrollingAnimation:horizontal];
661
+ [self waitForNativeResizeCommandsToSettle];
662
+ XCTAssertEqual([[self.pager valueForKey:@"currentIndex"] integerValue], 2);
663
+ XCTAssertTrue(horizontal.scrollEnabled);
664
+ XCTAssertEqualObjects(spy.selectedIndexes, (@[@0, @2]));
665
+ self.pager.frame = CGRectMake(0, 100, 400, 500);
666
+ [self.pager layoutSubviews];
667
+ XCTAssertEqualWithAccuracy(horizontal.contentOffset.x, 800, 0.001);
668
+ [self.pager scrollViewDidEndScrollingAnimation:horizontal];
669
+ XCTAssertEqualObjects(spy.selectedIndexes, (@[@0, @2]));
670
+ XCTAssertNil([self.pager valueForKey:@"pendingNativePagerScrollCompletion"]);
671
+ XCTAssertFalse([[self.pager valueForKey:@"transitioning"] boolValue]);
672
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
673
+ } else {
674
+ XCTSkip(@"Native local Hard animation requires iOS 27 or newer");
675
+ }
676
+ }
677
+
678
+ - (void)testExternalDirectDragPreservesProxyIdentityAndSettles
679
+ {
680
+ if (@available(iOS 27.0, *)) {
681
+ UIScrollView *source = nil;
682
+ UIView *overlay = nil;
683
+ UIScrollView *horizontal = [self prepareExternalDirectDragWithSource:&source overlay:&overlay];
684
+ UIView *proxy = [self.pager valueForKey:@"topOverlayHostView"];
685
+ UIView *pinned = [self.pager valueForKey:@"pinnedStickyHostView"];
686
+ UIScrollEdgeElementContainerInteraction *top = [self.pager valueForKey:@"topEdgeInteraction"];
687
+ UIScrollEdgeElementContainerInteraction *sticky = [self.pager valueForKey:@"stickyEdgeInteraction"];
688
+ UIView *marker = [self.pager valueForKey:@"topEdgeMarker"];
689
+ UIView *stickyMarker = [self.pager valueForKey:@"stickyEdgeMarker"];
690
+ NSArray<UIViewController *> *pages = [self.pager valueForKey:@"pageControllers"];
691
+ UIScrollView *target = (UIScrollView *)pages[2].view.subviews.firstObject;
692
+ XCTAssertNotNil(top);
693
+ XCTAssertNotNil(sticky);
694
+ XCTAssertTrue([proxy.interactions containsObject:top]);
695
+ XCTAssertEqual(overlay.interactions.count, 0);
696
+ [self.pager adoptCurrentScrollEdgeForProgrammaticTransition:NO];
697
+ [self.pager beginProgrammaticScrollEdgesToIndex:2 allowExternalOverlay:NO];
698
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
699
+ XCTAssertEqual([self.pager valueForKey:@"topEdgeInteraction"], top);
700
+
701
+ [self.pager scrollViewWillBeginDragging:horizontal];
702
+ NSArray *owners = [self.pager valueForKey:@"programmaticScrollEdgeOwners"];
703
+ XCTAssertEqual(owners.count, 3);
704
+ id sourceOwner = owners.firstObject;
705
+ XCTAssertEqual([sourceOwner valueForKey:@"topInteraction"], top);
706
+ XCTAssertEqual([sourceOwner valueForKey:@"stickyInteraction"], sticky);
707
+ XCTAssertEqual([sourceOwner valueForKey:@"topMarker"], marker);
708
+ XCTAssertEqual([sourceOwner valueForKey:@"topHost"], proxy);
709
+ XCTAssertEqual([sourceOwner valueForKey:@"originalStyle"], UIScrollEdgeEffectStyle.softStyle);
710
+ XCTAssertTrue([[sourceOwner valueForKey:@"borrowsPinnedStickyHost"] boolValue]);
711
+ XCTAssertFalse([[sourceOwner valueForKey:@"ownsTransitionHosts"] boolValue]);
712
+ XCTAssertEqual(top.scrollView, source);
713
+ XCTAssertEqual(sticky.scrollView, source);
714
+ XCTAssertEqual(marker.superview, proxy);
715
+ XCTAssertEqual(pinned.superview, source);
716
+ XCTAssertFalse([self.pager resolveExternalTopOverlay]);
717
+ [self.pager updateTopScrollEdgeForScrollView:source];
718
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 3);
719
+ XCTAssertEqual(overlay.superview, self.root);
720
+ XCTAssertEqual(overlay.interactions.count, 0);
721
+
722
+ NSMutableArray *incomingOwners = [NSMutableArray new];
723
+ NSMutableArray<UIView *> *ownedHosts = [NSMutableArray new];
724
+ NSMutableArray<UIView *> *ownedMarkers = [NSMutableArray new];
725
+ NSMutableArray<UIScrollEdgeElementContainerInteraction *> *ownedEdges = [NSMutableArray new];
726
+ for (id owner in owners) {
727
+ if (owner == sourceOwner) continue;
728
+ [incomingOwners addObject:owner];
729
+ XCTAssertTrue([[owner valueForKey:@"ownsTransitionHosts"] boolValue]);
730
+ UIView *page = [owner valueForKey:@"page"];
731
+ UIScrollView *list = [owner valueForKey:@"scrollView"];
732
+ UIView *ownedTop = [owner valueForKey:@"ownedTopHost"];
733
+ UIView *ownedSticky = [owner valueForKey:@"ownedStickyHost"];
734
+ XCTAssertNotNil(ownedTop);
735
+ XCTAssertNotNil(ownedSticky);
736
+ XCTAssertEqual([owner valueForKey:@"topHost"], ownedTop);
737
+ XCTAssertEqual([owner valueForKey:@"stickyHost"], ownedSticky);
738
+ XCTAssertNotEqual(ownedTop, ownedSticky);
739
+ for (NSString *edgeName in @[@"top", @"sticky"]) {
740
+ UIView *host = [owner valueForKey:[edgeName stringByAppendingString:@"Host"]];
741
+ UIView *edgeMarker = [owner valueForKey:[edgeName stringByAppendingString:@"Marker"]];
742
+ UIScrollEdgeElementContainerInteraction *edge =
743
+ [owner valueForKey:[edgeName stringByAppendingString:@"Interaction"]];
744
+ XCTAssertFalse([ownedHosts containsObject:host]);
745
+ [ownedHosts addObject:host];
746
+ [ownedMarkers addObject:edgeMarker];
747
+ [ownedEdges addObject:edge];
748
+ XCTAssertEqual(host.superview, page);
749
+ XCTAssertNotEqual(host, proxy);
750
+ XCTAssertNotEqual(host, self.sticky);
751
+ XCTAssertNotEqual(host, overlay);
752
+ XCTAssertFalse(host.userInteractionEnabled);
753
+ XCTAssertTrue(host.accessibilityElementsHidden);
754
+ if (host.backgroundColor != nil) {
755
+ XCTAssertEqualWithAccuracy(CGColorGetAlpha(host.backgroundColor.CGColor), 0, 0.001);
756
+ }
757
+ XCTAssertEqual(edge.view, host);
758
+ XCTAssertEqual(edge.scrollView, list);
759
+ XCTAssertEqual(edgeMarker.superview, host);
760
+ XCTAssertEqualWithAccuracy(host.frame.origin.x,
761
+ CGRectGetMinX([list convertRect:list.bounds toView:page]), 0.001);
762
+ }
763
+ CGRect pageFrame = page.frame;
764
+ CGRect listFrame = list.frame;
765
+ CGRect topFrame = ownedTop.frame;
766
+ CGRect stickyFrame = ownedSticky.frame;
767
+ page.frame = CGRectMake(pageFrame.origin.x, pageFrame.origin.y, 400, pageFrame.size.height);
768
+ list.frame = CGRectMake(listFrame.origin.x, listFrame.origin.y, 400, listFrame.size.height);
769
+ XCTAssertEqualWithAccuracy(ownedTop.frame.size.width, 400, 0.001);
770
+ XCTAssertEqualWithAccuracy(ownedSticky.frame.size.width, 400, 0.001);
771
+ XCTAssertEqualWithAccuracy(((UIView *)[owner valueForKey:@"topMarker"]).frame.size.width, 400, 0.001);
772
+ XCTAssertEqualWithAccuracy(((UIView *)[owner valueForKey:@"stickyMarker"]).frame.size.width, 400, 0.001);
773
+ XCTAssertEqualWithAccuracy(ownedTop.frame.origin.y, topFrame.origin.y, 0.001);
774
+ XCTAssertEqualWithAccuracy(ownedSticky.frame.origin.y, stickyFrame.origin.y, 0.001);
775
+ XCTAssertEqualWithAccuracy(ownedTop.frame.size.height, topFrame.size.height, 0.001);
776
+ XCTAssertEqualWithAccuracy(ownedSticky.frame.size.height, stickyFrame.size.height, 0.001);
777
+ page.frame = pageFrame;
778
+ list.frame = listFrame;
779
+ }
780
+ XCTAssertEqual(incomingOwners.count, 2);
781
+ XCTAssertEqual(ownedHosts.count, 4);
782
+ horizontal.contentOffset = CGPointMake(480, 0);
783
+ for (id owner in incomingOwners) {
784
+ UIScrollView *list = [owner valueForKey:@"scrollView"];
785
+ CGRect viewport = [list convertRect:list.bounds toView:nil];
786
+ UIView *ownedTop = [owner valueForKey:@"ownedTopHost"];
787
+ UIView *ownedSticky = [owner valueForKey:@"ownedStickyHost"];
788
+ CGRect topWindow = [ownedTop convertRect:ownedTop.bounds toView:nil];
789
+ CGRect stickyWindow = [ownedSticky convertRect:ownedSticky.bounds toView:nil];
790
+ XCTAssertNotEqualWithAccuracy(viewport.origin.x, 0, 0.001);
791
+ XCTAssertEqualWithAccuracy(topWindow.origin.x, viewport.origin.x, 0.001);
792
+ XCTAssertEqualWithAccuracy(stickyWindow.origin.x, viewport.origin.x, 0.001);
793
+ XCTAssertEqualWithAccuracy(topWindow.origin.y, viewport.origin.y, 0.001);
794
+ XCTAssertEqualWithAccuracy(stickyWindow.origin.y, viewport.origin.y + 50, 0.001);
795
+ }
796
+
797
+ horizontal.contentOffset = CGPointMake(640, 0);
798
+ [self.pager scrollViewDidEndDragging:horizontal willDecelerate:NO];
799
+ XCTestExpectation *settled = [self expectationWithDescription:@"external drag settles"];
800
+ dispatch_async(dispatch_get_main_queue(), ^{ [settled fulfill]; });
801
+ [self waitForExpectations:@[settled] timeout:2];
802
+ XCTAssertEqual([[self.pager valueForKey:@"currentIndex"] integerValue], 2);
803
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
804
+ for (id owner in incomingOwners) {
805
+ XCTAssertNil([owner valueForKey:@"ownedTopHost"]);
806
+ XCTAssertNil([owner valueForKey:@"ownedStickyHost"]);
807
+ }
808
+ for (UIView *host in ownedHosts) {
809
+ XCTAssertNil(host.superview);
810
+ XCTAssertEqual(host.interactions.count, 0);
811
+ }
812
+ for (UIView *edgeMarker in ownedMarkers) XCTAssertNil(edgeMarker.superview);
813
+ for (UIScrollEdgeElementContainerInteraction *edge in ownedEdges) XCTAssertNil(edge.scrollView);
814
+ XCTAssertEqual(proxy.superview, [self.pager valueForKey:@"containerView"]);
815
+ XCTAssertNotNil(self.sticky.superview);
816
+ XCTAssertNil(top.scrollView);
817
+ XCTAssertNil(sticky.scrollView);
818
+ XCTAssertNil(marker.superview);
819
+ XCTAssertEqual(source.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
820
+ UIScrollEdgeElementContainerInteraction *normal = [self.pager valueForKey:@"topEdgeInteraction"];
821
+ XCTAssertNotNil(normal);
822
+ XCTAssertEqual(normal.scrollView, target);
823
+ XCTAssertEqual([self.pager valueForKey:@"topEdgeScrollView"], target);
824
+ XCTAssertEqual([self.pager valueForKey:@"originalTopEdgeStyle"], UIScrollEdgeEffectStyle.softStyle);
825
+ XCTAssertEqual(target.topEdgeEffect.style, UIScrollEdgeEffectStyle.hardStyle);
826
+ UIScrollView *otherNeighbor = (UIScrollView *)pages[0].view.subviews.firstObject;
827
+ XCTAssertEqual(otherNeighbor.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
828
+ XCTAssertNil(stickyMarker.superview);
829
+ XCTAssertEqual([self.pager valueForKey:@"topEdgeInteractionHost"], proxy);
830
+ XCTAssertEqual(overlay.superview, self.root);
831
+ XCTAssertEqual(overlay.interactions.count, 0);
832
+ } else {
833
+ XCTSkip(@"External direct Hard drag handoff requires iOS 27 or newer");
834
+ }
835
+ }
836
+
837
+ - (void)testExternalOverlayGeometryChangeReleasesBorrowedEdges
838
+ {
839
+ if (@available(iOS 27.0, *)) {
840
+ UIScrollView *source = nil;
841
+ UIView *overlay = nil;
842
+ UIScrollView *horizontal = [self prepareExternalDirectDragWithSource:&source overlay:&overlay];
843
+ UIScrollEdgeElementContainerInteraction *top = [self.pager valueForKey:@"topEdgeInteraction"];
844
+ UIView *pinned = [self.pager valueForKey:@"pinnedStickyHostView"];
845
+ [self.pager scrollViewWillBeginDragging:horizontal];
846
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 3);
847
+ id incoming = [[self.pager valueForKey:@"programmaticScrollEdgeOwners"] lastObject];
848
+ UIView *ownedTop = [incoming valueForKey:@"ownedTopHost"];
849
+ UIView *ownedSticky = [incoming valueForKey:@"ownedStickyHost"];
850
+ UIView *ownedMarker = [incoming valueForKey:@"topMarker"];
851
+ UIScrollEdgeElementContainerInteraction *ownedEdge = [incoming valueForKey:@"topInteraction"];
852
+ UIView *proxy = [self.pager valueForKey:@"topOverlayHostView"];
853
+ UIView *stickyParent = self.sticky.superview;
854
+ XCTAssertNotNil(ownedTop);
855
+ XCTAssertNotNil(ownedSticky);
856
+ overlay.frame = CGRectMake(0, 100, 320, 60);
857
+ XCTAssertTrue([self.pager resolveExternalTopOverlay]);
858
+ XCTAssertEqualWithAccuracy([[self.pager valueForKey:@"externalTopOverlayHeight"] doubleValue], 60, 0.001);
859
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
860
+ XCTAssertNil(top.scrollView);
861
+ XCTAssertNil(ownedTop.superview);
862
+ XCTAssertNil(ownedSticky.superview);
863
+ XCTAssertEqual(ownedTop.interactions.count, 0);
864
+ XCTAssertEqual(ownedSticky.interactions.count, 0);
865
+ XCTAssertNil(ownedMarker.superview);
866
+ XCTAssertNil(ownedEdge.scrollView);
867
+ XCTAssertNil([incoming valueForKey:@"ownedTopHost"]);
868
+ XCTAssertNil([incoming valueForKey:@"ownedStickyHost"]);
869
+ XCTAssertEqual(proxy.superview, [self.pager valueForKey:@"containerView"]);
870
+ XCTAssertEqual(self.sticky.superview, stickyParent);
871
+ XCTAssertEqual(overlay.superview, self.root);
872
+ XCTAssertEqual(source.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
873
+ XCTAssertEqual(pinned.superview, [self.pager valueForKey:@"containerView"]);
874
+ XCTAssertTrue(pinned.hidden);
875
+ } else {
876
+ XCTSkip(@"External direct Hard drag handoff requires iOS 27 or newer");
877
+ }
878
+ }
879
+
880
+ - (void)testExternalOverlayReplacementReleasesBorrowedEdges
881
+ {
882
+ if (@available(iOS 27.0, *)) {
883
+ UIScrollView *source = nil;
884
+ UIView *overlay = nil;
885
+ UIScrollView *horizontal = [self prepareExternalDirectDragWithSource:&source overlay:&overlay];
886
+ UIScrollEdgeElementContainerInteraction *top = [self.pager valueForKey:@"topEdgeInteraction"];
887
+ XCTAssertNotNil(top);
888
+ [self.pager scrollViewWillBeginDragging:horizontal];
889
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 3);
890
+ UIView *replacement = [[UIView alloc] initWithFrame:overlay.frame];
891
+ replacement.tag = overlay.tag;
892
+ [overlay removeFromSuperview];
893
+ [self.root addSubview:replacement];
894
+ XCTAssertTrue([self.pager resolveExternalTopOverlay]);
895
+ XCTAssertEqual([self.pager valueForKey:@"externalTopOverlayView"], replacement);
896
+ XCTAssertEqualWithAccuracy([[self.pager valueForKey:@"externalTopOverlayHeight"] doubleValue], 50, 0.001);
897
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
898
+ XCTAssertNil(top.scrollView);
899
+ XCTAssertEqual(source.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
900
+ XCTAssertEqual(replacement.interactions.count, 0);
901
+ XCTAssertEqual(replacement.superview, self.root);
902
+ } else {
903
+ XCTSkip(@"External direct Hard drag handoff requires iOS 27 or newer");
904
+ }
905
+ }
906
+
907
+ - (void)testExternalOverlayTagChangeReleasesBorrowedEdges
908
+ {
909
+ if (@available(iOS 27.0, *)) {
910
+ UIScrollView *source = nil;
911
+ UIScrollView *horizontal = [self prepareExternalDirectDragWithSource:&source overlay:nil];
912
+ UIScrollEdgeElementContainerInteraction *top = [self.pager valueForKey:@"topEdgeInteraction"];
913
+ XCTAssertNotNil(top);
914
+ [self.pager scrollViewWillBeginDragging:horizontal];
915
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 3);
916
+ auto props = std::make_shared<RNCCollapsiblePagerViewProps>();
917
+ props->nativeSmoothHeaderScrollEnabled = true;
918
+ props->topScrollEdgeEffect = RNCCollapsiblePagerViewTopScrollEdgeEffect::Hard;
919
+ props->externalTopOverlayTag = 0;
920
+ [self.pager updateProps:props oldProps:nullptr];
921
+ XCTAssertEqual([[self.pager valueForKey:@"externalTopOverlayTag"] integerValue], 0);
922
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
923
+ XCTAssertNil(top.scrollView);
924
+ XCTAssertEqual(source.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
925
+ } else {
926
+ XCTSkip(@"External direct Hard drag handoff requires iOS 27 or newer");
927
+ }
928
+ }
929
+
930
+ - (void)testExternalOuterInactivityReleasesBorrowedEdges
931
+ {
932
+ if (@available(iOS 27.0, *)) {
933
+ UIScrollView *source = nil;
934
+ UIScrollView *horizontal = [self prepareExternalDirectDragWithSource:&source overlay:nil];
935
+ RNCPagerViewComponentView *outer = [RNCPagerViewComponentView new];
936
+ [self.pager setValue:outer forKey:@"externalOuterPager"];
937
+ [self.pager setValue:@YES forKey:@"externalOuterPageActive"];
938
+ [self.pager setValue:@NO forKey:@"externalOuterTransitioning"];
939
+ UIScrollEdgeElementContainerInteraction *top = [self.pager valueForKey:@"topEdgeInteraction"];
940
+ XCTAssertNotNil(top);
941
+ [self.pager scrollViewWillBeginDragging:horizontal];
942
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 3);
943
+ [self.pager outerPagerDidUpdateOverlayOwner:NO transitioning:NO];
944
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
945
+ XCTAssertNil(top.scrollView);
946
+ XCTAssertNil([self.pager valueForKey:@"topEdgeInteraction"]);
947
+ XCTAssertEqual(source.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
948
+ [self.pager beginProgrammaticScrollEdgesToIndex:2 allowExternalOverlay:YES];
949
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
950
+ } else {
951
+ XCTSkip(@"External direct Hard drag handoff requires iOS 27 or newer");
952
+ }
953
+ }
954
+
955
+ - (void)testExternalOuterTransitionReleasesBorrowedEdges
956
+ {
957
+ if (@available(iOS 27.0, *)) {
958
+ UIScrollView *source = nil;
959
+ UIScrollView *horizontal = [self prepareExternalDirectDragWithSource:&source overlay:nil];
960
+ RNCPagerViewComponentView *outer = [RNCPagerViewComponentView new];
961
+ [self.pager setValue:outer forKey:@"externalOuterPager"];
962
+ [self.pager setValue:@YES forKey:@"externalOuterPageActive"];
963
+ [self.pager setValue:@NO forKey:@"externalOuterTransitioning"];
964
+ UIScrollEdgeElementContainerInteraction *top = [self.pager valueForKey:@"topEdgeInteraction"];
965
+ XCTAssertNotNil(top);
966
+ [self.pager scrollViewWillBeginDragging:horizontal];
967
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 3);
968
+ [self.pager outerPagerDidUpdateOverlayOwner:YES transitioning:YES];
969
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
970
+ XCTAssertNil(top.scrollView);
971
+ XCTAssertNil([self.pager valueForKey:@"topEdgeInteraction"]);
972
+ XCTAssertEqual(source.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
973
+ [self.pager beginProgrammaticScrollEdgesToIndex:2 allowExternalOverlay:YES];
974
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
975
+ } else {
976
+ XCTSkip(@"External direct Hard drag handoff requires iOS 27 or newer");
977
+ }
978
+ }
979
+
980
+ - (void)testDirectDragAdoptsSourceBeforeDetachAndOwnsBothNeighbours
981
+ {
982
+ if (@available(iOS 27.0, *)) {
983
+ UIScrollView *source = nil;
984
+ UIScrollView *horizontal = [self prepareDirectDragWithSource:&source];
985
+ UIView *topHost = [self.pager valueForKey:@"topEdgeInteractionHost"];
986
+ UIView *pinnedHost = [self.pager valueForKey:@"pinnedStickyHostView"];
987
+ UIScrollEdgeElementContainerInteraction *top = [self.pager valueForKey:@"topEdgeInteraction"];
988
+ UIScrollEdgeElementContainerInteraction *sticky = [self.pager valueForKey:@"stickyEdgeInteraction"];
989
+ UIView *topMarker = [self.pager valueForKey:@"topEdgeMarker"];
990
+ UIView *stickyMarker = [self.pager valueForKey:@"stickyEdgeMarker"];
991
+ XCTAssertNotNil(top);
992
+ XCTAssertNotNil(sticky);
993
+
994
+ [self.pager scrollViewWillBeginDragging:horizontal];
995
+
996
+ NSArray *owners = [self.pager valueForKey:@"programmaticScrollEdgeOwners"];
997
+ XCTAssertEqual(owners.count, 3);
998
+ id sourceOwner = [owners firstObject];
999
+ XCTAssertEqual([sourceOwner valueForKey:@"topInteraction"], top);
1000
+ XCTAssertEqual([sourceOwner valueForKey:@"stickyInteraction"], sticky);
1001
+ XCTAssertEqual([sourceOwner valueForKey:@"topMarker"], topMarker);
1002
+ XCTAssertEqual([sourceOwner valueForKey:@"stickyMarker"], stickyMarker);
1003
+ XCTAssertEqual([sourceOwner valueForKey:@"originalStyle"], UIScrollEdgeEffectStyle.softStyle);
1004
+ XCTAssertTrue([[sourceOwner valueForKey:@"borrowsPinnedStickyHost"] boolValue]);
1005
+ XCTAssertTrue([topHost.interactions containsObject:top]);
1006
+ XCTAssertTrue([pinnedHost.interactions containsObject:sticky]);
1007
+ XCTAssertEqual(top.scrollView, source);
1008
+ XCTAssertEqual(sticky.scrollView, source);
1009
+ XCTAssertEqual(topMarker.superview, topHost);
1010
+ XCTAssertEqual(stickyMarker.superview, pinnedHost);
1011
+ XCTAssertEqual(pinnedHost.superview, source);
1012
+ XCTAssertFalse(pinnedHost.hidden);
1013
+ for (id owner in owners) {
1014
+ XCTAssertNotNil([owner valueForKey:@"topInteraction"]);
1015
+ XCTAssertNotNil([[owner valueForKey:@"topInteraction"] scrollView]);
1016
+ }
1017
+ } else {
1018
+ XCTSkip(@"Direct local Hard drag handoff requires iOS 27 or newer");
1019
+ }
1020
+ }
1021
+
1022
+ - (void)testDirectDragTakeoverIgnoresOldSettlementAndReleasesOnReturn
1023
+ {
1024
+ if (@available(iOS 27.0, *)) {
1025
+ UIScrollView *source = nil;
1026
+ UIScrollView *horizontal = [self prepareDirectDragWithSource:&source];
1027
+ UIScrollEdgeElementContainerInteraction *top = [self.pager valueForKey:@"topEdgeInteraction"];
1028
+ UIView *topHost = [self.pager valueForKey:@"topEdgeInteractionHost"];
1029
+ NSUInteger originalTransition = [[self.pager valueForKey:@"transitionId"] unsignedIntegerValue];
1030
+ XCTAssertEqual([[self.pager valueForKey:@"pageControllers"] count], 3);
1031
+ XCTAssertEqualWithAccuracy(horizontal.contentOffset.x, 320, 0.001);
1032
+ XCTAssertEqual([[self.pager valueForKey:@"currentIndex"] integerValue], 1);
1033
+ [self.pager scrollViewWillBeginDragging:horizontal];
1034
+ [self.pager scrollViewDidEndDragging:horizontal willDecelerate:NO];
1035
+ [self.pager scrollViewWillBeginDragging:horizontal];
1036
+ XCTAssertGreaterThan([[self.pager valueForKey:@"transitionId"] unsignedIntegerValue], originalTransition + 1);
1037
+ XCTAssertTrue([topHost.interactions containsObject:top]);
1038
+ XCTAssertEqual(top.scrollView, source);
1039
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 3);
1040
+
1041
+ XCTestExpectation *oldSettlement = [self expectationWithDescription:@"old settlement skipped"];
1042
+ dispatch_async(dispatch_get_main_queue(), ^{ [oldSettlement fulfill]; });
1043
+ [self waitForExpectations:@[oldSettlement] timeout:2];
1044
+ XCTAssertEqualWithAccuracy(horizontal.contentOffset.x, 320, 0.001);
1045
+ XCTAssertEqual([[self.pager valueForKey:@"currentIndex"] integerValue], 1);
1046
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 3);
1047
+ [self.pager scrollViewDidEndDragging:horizontal willDecelerate:YES];
1048
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 3);
1049
+ [self.pager scrollViewDidEndDecelerating:horizontal];
1050
+ XCTestExpectation *settled = [self expectationWithDescription:@"drag settled"];
1051
+ dispatch_async(dispatch_get_main_queue(), ^{ [settled fulfill]; });
1052
+ [self waitForExpectations:@[settled] timeout:2];
1053
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
1054
+ XCTAssertFalse([topHost.interactions containsObject:top]);
1055
+ XCTAssertNil(top.scrollView);
1056
+ XCTAssertEqual([[self.pager valueForKey:@"currentIndex"] integerValue], 1);
1057
+ XCTAssertEqual([self.pager valueForKey:@"topEdgeScrollView"], source);
1058
+ XCTAssertEqual([self.pager valueForKey:@"originalTopEdgeStyle"], UIScrollEdgeEffectStyle.softStyle);
1059
+ UIScrollEdgeElementContainerInteraction *normal = [self.pager valueForKey:@"topEdgeInteraction"];
1060
+ XCTAssertNotNil(normal);
1061
+ XCTAssertEqual(normal.scrollView, source);
1062
+ XCTAssertEqual(source.topEdgeEffect.style, UIScrollEdgeEffectStyle.hardStyle);
1063
+ NSArray<UIViewController *> *pages = [self.pager valueForKey:@"pageControllers"];
1064
+ for (NSNumber *index in @[@0, @2]) {
1065
+ UIScrollView *neighbor = (UIScrollView *)pages[index.integerValue].view.subviews.firstObject;
1066
+ XCTAssertEqual(neighbor.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
1067
+ }
1068
+ } else {
1069
+ XCTSkip(@"Direct local Hard drag handoff requires iOS 27 or newer");
1070
+ }
1071
+ }
1072
+
1073
+ - (void)testAnimatedDirectPageCommandBorrowsRealSourceEdgeUntilCompletion
1074
+ {
1075
+ if (@available(iOS 27.0, *)) {
1076
+ UIScrollView *source = [self preparePinnedLocalHardSource];
1077
+ UIView *topHost = [self.pager valueForKey:@"topEdgeInteractionHost"];
1078
+ UIView *pinnedHost = [self.pager valueForKey:@"pinnedStickyHostView"];
1079
+ UIScrollEdgeElementContainerInteraction *top = [self.pager valueForKey:@"topEdgeInteraction"];
1080
+ UIScrollEdgeElementContainerInteraction *sticky = [self.pager valueForKey:@"stickyEdgeInteraction"];
1081
+ UIView *topMarker = [self.pager valueForKey:@"topEdgeMarker"];
1082
+ UIView *stickyMarker = [self.pager valueForKey:@"stickyEdgeMarker"];
1083
+ XCTAssertNotNil(top);
1084
+ XCTAssertNotNil(sticky);
1085
+ XCTAssertEqual([self.pager valueForKey:@"originalTopEdgeStyle"], UIScrollEdgeEffectStyle.softStyle);
1086
+
1087
+ [self.pager adoptCurrentScrollEdgeForProgrammaticTransition:NO];
1088
+ id owner = [[self.pager valueForKey:@"programmaticScrollEdgeOwners"] firstObject];
1089
+ XCTAssertNotNil(owner);
1090
+ XCTAssertEqual([owner valueForKey:@"topInteraction"], top);
1091
+ XCTAssertEqual([owner valueForKey:@"stickyInteraction"], sticky);
1092
+ XCTAssertEqual([owner valueForKey:@"topMarker"], topMarker);
1093
+ XCTAssertEqual([owner valueForKey:@"stickyMarker"], stickyMarker);
1094
+ XCTAssertEqual([owner valueForKey:@"originalStyle"], UIScrollEdgeEffectStyle.softStyle);
1095
+ XCTAssertTrue([[owner valueForKey:@"borrowsPinnedStickyHost"] boolValue]);
1096
+ XCTAssertTrue([topHost.interactions containsObject:top]);
1097
+ XCTAssertTrue([pinnedHost.interactions containsObject:sticky]);
1098
+ XCTAssertEqual(top.scrollView, source);
1099
+ XCTAssertEqual(sticky.scrollView, source);
1100
+ XCTAssertEqual(topMarker.superview, topHost);
1101
+ XCTAssertEqual(stickyMarker.superview, pinnedHost);
1102
+ XCTAssertEqual(source.topEdgeEffect.style, UIScrollEdgeEffectStyle.hardStyle);
1103
+
1104
+ [self.pager restorePinnedStickyHeader];
1105
+ XCTAssertEqual(pinnedHost.superview, source);
1106
+ XCTAssertFalse(pinnedHost.hidden);
1107
+ [self.pager setValue:nil forKey:@"observedScrollView"];
1108
+ [self.pager finishProgrammaticScrollEdges];
1109
+ XCTAssertFalse([topHost.interactions containsObject:top]);
1110
+ XCTAssertFalse([pinnedHost.interactions containsObject:sticky]);
1111
+ XCTAssertNil(top.scrollView);
1112
+ XCTAssertNil(sticky.scrollView);
1113
+ XCTAssertNil(topMarker.superview);
1114
+ XCTAssertNil(stickyMarker.superview);
1115
+ XCTAssertEqual(source.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
1116
+ XCTAssertEqual(pinnedHost.superview, [self.pager valueForKey:@"containerView"]);
1117
+ XCTAssertTrue(pinnedHost.hidden);
1118
+ XCTAssertEqual([[self.pager valueForKey:@"programmaticScrollEdgeOwners"] count], 0);
1119
+ } else {
1120
+ XCTSkip(@"Animated local Hard edge handoff requires iOS 27 or newer");
1121
+ }
1122
+ }
1123
+
1124
+ - (void)testCancelledDirectPageCommandReleasesBorrowedSourceEdge
1125
+ {
1126
+ if (@available(iOS 27.0, *)) {
1127
+ UIScrollView *source = [self preparePinnedLocalHardSource];
1128
+ UIView *topHost = [self.pager valueForKey:@"topEdgeInteractionHost"];
1129
+ UIView *pinnedHost = [self.pager valueForKey:@"pinnedStickyHostView"];
1130
+ UIScrollEdgeElementContainerInteraction *top = [self.pager valueForKey:@"topEdgeInteraction"];
1131
+ UIScrollEdgeElementContainerInteraction *sticky = [self.pager valueForKey:@"stickyEdgeInteraction"];
1132
+ UIView *topMarker = [self.pager valueForKey:@"topEdgeMarker"];
1133
+ UIView *stickyMarker = [self.pager valueForKey:@"stickyEdgeMarker"];
1134
+ [self.pager adoptCurrentScrollEdgeForProgrammaticTransition:NO];
1135
+ [self.pager restorePinnedStickyHeader];
1136
+ [self.pager clearProgrammaticScrollEdges];
1137
+ XCTAssertFalse([topHost.interactions containsObject:top]);
1138
+ XCTAssertFalse([pinnedHost.interactions containsObject:sticky]);
1139
+ XCTAssertNil(topMarker.superview);
1140
+ XCTAssertNil(stickyMarker.superview);
1141
+ XCTAssertEqual(source.topEdgeEffect.style, UIScrollEdgeEffectStyle.softStyle);
1142
+ XCTAssertEqual(pinnedHost.superview, [self.pager valueForKey:@"containerView"]);
1143
+ XCTAssertTrue(pinnedHost.hidden);
1144
+ } else {
1145
+ XCTSkip(@"Animated local Hard edge handoff requires iOS 27 or newer");
1146
+ }
1147
+ }
1148
+
393
1149
  - (void)testIOS27LocalHardThemeBackgroundTracksMaskedHeaderGeometry
394
1150
  {
395
1151
  if (@available(iOS 27.0, *)) {