@onekeyfe/react-native-pager-view 6.7.0-23 → 6.7.0-24

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 (25) hide show
  1. package/README.md +16 -0
  2. package/android/build.gradle +1 -0
  3. package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +170 -0
  4. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +88 -12
  5. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +447 -15
  6. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +11 -1
  7. package/android/src/main/java/com/reactnativepagerview/event/NativeHeaderPressEvent.kt +8 -0
  8. package/docs/SPEC.md +333 -3
  9. package/ios/RNCCollapsiblePagerViewComponentView.mm +847 -21
  10. package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +116 -0
  11. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +22 -0
  12. package/lib/module/CollapsiblePagerView.js +37 -5
  13. package/lib/module/CollapsiblePagerView.web.js +520 -6
  14. package/lib/module/CollapsiblePagerViewNativeComponent.ts +6 -0
  15. package/lib/module/NativeScrollerNativeComponent.ts +0 -1
  16. package/lib/typescript/src/CollapsiblePagerView.d.ts +31 -2
  17. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +44 -1
  18. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +11 -0
  19. package/lib/typescript/src/NativeScroller.d.ts +2 -2
  20. package/package.json +3 -2
  21. package/src/CollapsiblePagerView.tsx +68 -5
  22. package/src/CollapsiblePagerView.web.tsx +465 -5
  23. package/src/CollapsiblePagerViewNativeComponent.ts +6 -0
  24. package/src/NativeScroller.tsx +2 -3
  25. package/src/NativeScrollerNativeComponent.ts +0 -1
@@ -14,6 +14,8 @@
14
14
  #import "React/RCTSurfaceTouchHandler.h"
15
15
  #import "React/RCTTouchHandler.h"
16
16
  #import <objc/runtime.h>
17
+ #import <React/RCTConvert.h>
18
+ #import <React/RCTFont.h>
17
19
  #import <UIKit/UIGestureRecognizerSubclass.h>
18
20
 
19
21
  // OneKey patch: deduplicate structural mutation parents before scanning the page.
@@ -398,8 +400,65 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
398
400
  (long)pagerState]);
399
401
  }
400
402
 
403
+ static CGFloat RNCPagerToolbarDimension(NSDictionary *entry, NSString *key, CGFloat fallback, CGFloat minimum, CGFloat maximum)
404
+ {
405
+ id value = entry[key];
406
+ if (![value isKindOfClass:NSNumber.class]) return fallback;
407
+ double dimension = [value doubleValue];
408
+ return std::isfinite(dimension) ? MIN(maximum, MAX(minimum, dimension)) : fallback;
409
+ }
410
+
411
+ @protocol RNCPagerNamedSVGBridge <NSObject>
412
+ + (void)cancelWithOwner:(id)owner;
413
+ + (void)requestWithOwner:(id)owner name:(NSString *)name color:(NSString *)color
414
+ width:(NSInteger)width height:(NSInteger)height scale:(double)scale
415
+ complete:(void (^)(UIImage *))complete;
416
+ @end
417
+
418
+ @interface RNCPagerToolbarButton : UIButton
419
+ @property (nonatomic, strong) UIColor *baseColor;
420
+ @property (nonatomic, strong) UIColor *hoverColor;
421
+ @property (nonatomic, strong) UIColor *pressedColor;
422
+ @property (nonatomic) BOOL hovering;
423
+ @property (nonatomic) CGFloat iconSize;
424
+ - (void)refreshBackground;
425
+ @end
426
+ @implementation RNCPagerToolbarButton
427
+ - (instancetype)initWithFrame:(CGRect)frame {
428
+ if ((self = [super initWithFrame:frame])) {
429
+ self.adjustsImageWhenHighlighted = NO;
430
+ [self addGestureRecognizer:[[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hover:)]];
431
+ }
432
+ return self;
433
+ }
434
+ - (CGRect)imageRectForContentRect:(CGRect)contentRect {
435
+ CGFloat side = MIN(self.iconSize, MIN(CGRectGetWidth(contentRect), CGRectGetHeight(contentRect)));
436
+ return CGRectMake(CGRectGetMidX(contentRect) - side / 2, CGRectGetMidY(contentRect) - side / 2, side, side);
437
+ }
438
+ - (void)hover:(UIHoverGestureRecognizer *)gesture {
439
+ self.hovering = gesture.state == UIGestureRecognizerStateBegan || gesture.state == UIGestureRecognizerStateChanged;
440
+ [self refreshBackground];
441
+ }
442
+ - (void)setEnabled:(BOOL)enabled {
443
+ [super setEnabled:enabled];
444
+ if (!enabled) self.hovering = NO;
445
+ [self refreshBackground];
446
+ }
447
+ - (void)setHighlighted:(BOOL)highlighted {
448
+ [super setHighlighted:highlighted];
449
+ [self refreshBackground];
450
+ }
451
+ - (void)refreshBackground {
452
+ self.backgroundColor = self.enabled && self.highlighted ? self.pressedColor : (self.enabled && self.hovering ? self.hoverColor : self.baseColor);
453
+ }
454
+ @end
455
+
401
456
  @interface RNCCollapsiblePagerNativeTabBarView : UIView <UIScrollViewDelegate>
402
457
  @property (nonatomic, copy) RNCCollapsiblePagerNativeTabPressHandler onTabPress;
458
+ @property (nonatomic, copy) void (^onToolbarPress)(NSString *, CGRect);
459
+ @property (nonatomic, copy) void (^onToolbarNeedsSettlement)(void);
460
+ - (void)updateToolbarPagerIndex:(NSInteger)index busy:(BOOL)busy;
461
+ - (void)updateConfigJSON:(NSString *)json;
403
462
  - (void)updateItemsJSON:(NSString *)itemsJSON;
404
463
  - (void)updateStyleWithHeight:(CGFloat)height
405
464
  variant:(NSString *)variant
@@ -415,6 +474,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
415
474
  indicatorHeight:(CGFloat)indicatorHeight
416
475
  indicatorBottom:(CGFloat)indicatorBottom;
417
476
  - (void)setProgress:(CGFloat)progress;
477
+ - (BOOL)usesControlledTextProgress;
478
+ - (BOOL)ownsSettledToolbar;
418
479
  - (void)setLeftToRight:(BOOL)leftToRight;
419
480
  - (void)updateAncestorGesturePrecedence;
420
481
  @end
@@ -430,6 +491,40 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
430
491
  CGFloat _contentPaddingHorizontal;
431
492
  CGFloat _contentPaddingVertical;
432
493
  BOOL _segmented;
494
+ BOOL _textVariant;
495
+ NSDictionary *_config;
496
+ RNCPagerToolbarButton *_toolbarButton;
497
+ NSString *_toolbarKey;
498
+ BOOL _toolbarPagerBusy;
499
+ NSInteger _toolbarPageIndex;
500
+ NSDictionary *_committedToolbarEntry;
501
+ NSString *_desiredToolbarKey;
502
+ NSDictionary *_desiredToolbarEntry;
503
+ UIImage *_loadedToolbarImage;
504
+ BOOL _toolbarPresenceAnimating;
505
+ BOOL _toolbarPresenceInterrupted;
506
+ NSString *_toolbarTouchKey;
507
+ BOOL _toolbarTouchStarted;
508
+ NSString *_iconIdentity;
509
+ NSObject *_iconOwner;
510
+ NSInteger _progressSource;
511
+ NSInteger _progressDestination;
512
+ CGFloat _progressFraction;
513
+ CGFloat _nativeProgress;
514
+ BOOL _hasNativeProgress;
515
+ BOOL _hasMappedPageProgress;
516
+ CGFloat _toolbarReservedWidth;
517
+ BOOL _toolbarVisible;
518
+ NSUInteger _toolbarAnimationGeneration;
519
+ NSString *_loadedIconIdentity;
520
+ BOOL _iconRequestPending;
521
+ UIImage *_readyToolbarImage;
522
+ NSString *_readyToolbarIdentity;
523
+ NSString *_failedIconIdentity;
524
+ CGFloat _toolbarSize;
525
+ CGFloat _toolbarIconSize;
526
+ CGFloat _toolbarContentWidth;
527
+ CGFloat _toolbarTrailingInset;
433
528
  CGFloat _itemSpacing;
434
529
  CGFloat _fontSize;
435
530
  NSString *_fontFamily;
@@ -448,6 +543,21 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
448
543
  {
449
544
  if (self = [super initWithFrame:frame]) {
450
545
  _items = @[];
546
+ _config = @{};
547
+ _toolbarPagerBusy = YES;
548
+ _toolbarPageIndex = -1;
549
+ _toolbarButton = [RNCPagerToolbarButton new];
550
+ _toolbarButton.layer.cornerRadius = 19;
551
+ _toolbarButton.hidden = YES;
552
+ _toolbarButton.alpha = 0;
553
+ _toolbarButton.userInteractionEnabled = NO;
554
+ _toolbarButton.accessibilityElementsHidden = YES;
555
+ _toolbarButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
556
+ _toolbarButton.iconSize = 24;
557
+ [_toolbarButton addTarget:self action:@selector(toolbarTouchDown) forControlEvents:UIControlEventTouchDown];
558
+ [_toolbarButton addTarget:self action:@selector(toolbarPress:forEvent:) forControlEvents:UIControlEventTouchUpInside];
559
+ [_toolbarButton addTarget:self action:@selector(toolbarCancel) forControlEvents:UIControlEventTouchCancel | UIControlEventTouchUpOutside];
560
+ [self addSubview:_toolbarButton];
451
561
  _buttons = [NSMutableArray new];
452
562
  _indicatorFrames = [NSMutableArray new];
453
563
  _barHeight = 44;
@@ -495,6 +605,18 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
495
605
  return [UIFont systemFontOfSize:_fontSize weight:UIFontWeightMedium];
496
606
  }
497
607
 
608
+ - (UIFont *)activeTabFont
609
+ {
610
+ NSString *activeFamily = _config[@"activeFontFamily"];
611
+ if (activeFamily.length > 0) {
612
+ return [UIFont fontWithName:activeFamily size:_fontSize]
613
+ ?: [UIFont systemFontOfSize:_fontSize weight:UIFontWeightSemibold];
614
+ }
615
+ UIFont *normalFont = _fontFamily.length > 0 ? [UIFont fontWithName:_fontFamily size:_fontSize] : nil;
616
+ return normalFont != nil ? [RCTFont updateFont:normalFont withWeight:@"600"]
617
+ : [UIFont systemFontOfSize:_fontSize weight:UIFontWeightSemibold];
618
+ }
619
+
498
620
  - (void)updateItemsJSON:(NSString *)itemsJSON
499
621
  {
500
622
  NSData *data = [itemsJSON dataUsingEncoding:NSUTF8StringEncoding];
@@ -529,6 +651,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
529
651
  for (UIButton *button in _buttons) [_scrollView bringSubviewToFront:button];
530
652
  _shouldCenterSelectedItem = YES;
531
653
  self.hidden = _items.count == 0;
654
+ if (_hasNativeProgress) [self setProgress:_nativeProgress];
655
+ else if ([_config[@"controlled"] boolValue]) {
656
+ NSUInteger selected = [_items indexOfObjectPassingTest:^BOOL(NSDictionary *item, NSUInteger i, BOOL *stop) { return [item[@"key"] isEqual:self->_config[@"selectedKey"]]; }];
657
+ if (selected != NSNotFound) { _progress = selected; _progressSource = selected; _progressDestination = selected; _progressFraction = 0; }
658
+ }
532
659
  [self setNeedsLayout];
533
660
  }
534
661
 
@@ -549,13 +676,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
549
676
  _barHeight = MAX(1, height);
550
677
  BOOL wasSegmented = _segmented;
551
678
  _segmented = [variant isEqualToString:@"segmented"];
679
+ _textVariant = [variant isEqualToString:@"text"];
552
680
  _contentPaddingHorizontal = MAX(0, contentPaddingHorizontal);
553
681
  _contentPaddingVertical = MAX(0, contentPaddingVertical);
554
682
  _itemSpacing = MAX(0, itemSpacing);
555
683
  _fontSize = MAX(1, fontSize);
556
684
  _fontFamily = [fontFamily copy];
557
685
  self.backgroundColor = _segmented ? UIColor.clearColor : (backgroundColor ?: UIColor.clearColor);
558
- _scrollView.backgroundColor = self.backgroundColor;
686
+ _scrollView.backgroundColor = _textVariant ? UIColor.clearColor : self.backgroundColor;
559
687
  _scrollView.scrollEnabled = !_segmented;
560
688
  if (_segmented && !wasSegmented) [_scrollView setContentOffset:CGPointZero animated:NO];
561
689
  _trackView.hidden = !_segmented;
@@ -624,9 +752,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
624
752
  - (void)layoutSubviews
625
753
  {
626
754
  [super layoutSubviews];
627
- _scrollView.frame = self.bounds;
755
+ CGFloat reserved = _toolbarReservedWidth;
756
+ [self bringSubviewToFront:_toolbarButton];
757
+ _scrollView.frame = CGRectMake(_leftToRight ? 0 : reserved, 0,
758
+ MAX(0, CGRectGetWidth(self.bounds) - reserved), CGRectGetHeight(self.bounds));
759
+ CGFloat inset = _toolbarTrailingInset - (_toolbarSize - _toolbarContentWidth) / 2;
760
+ _toolbarButton.frame = CGRectMake(_leftToRight ? CGRectGetWidth(self.bounds) - inset - _toolbarSize : inset,
761
+ (CGRectGetHeight(self.bounds) - _toolbarSize) / 2, _toolbarSize, _toolbarSize);
628
762
  if (_segmented) {
629
- CGFloat width = CGRectGetWidth(self.bounds);
763
+ CGFloat width = CGRectGetWidth(_scrollView.bounds);
630
764
  CGFloat height = CGRectGetHeight(self.bounds);
631
765
  CGFloat insetX = MIN(_contentPaddingHorizontal, width / 2);
632
766
  CGFloat insetY = MIN(_contentPaddingVertical, height / 2);
@@ -634,7 +768,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
634
768
  MAX(0, width - 2 * insetX), MAX(0, height - 2 * insetY));
635
769
  _trackView.frame = trackFrame;
636
770
  _trackView.layer.cornerRadius = CGRectGetHeight(trackFrame) / 2;
637
- _scrollView.contentSize = self.bounds.size;
771
+ _scrollView.contentSize = _scrollView.bounds.size;
638
772
  [_indicatorFrames removeAllObjects];
639
773
  NSUInteger count = _buttons.count;
640
774
  [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
@@ -658,12 +792,16 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
658
792
  for (UIButton *button in _buttons) {
659
793
  NSString *title = [button titleForState:UIControlStateNormal] ?: @"";
660
794
  CGFloat textWidth = ceil([title sizeWithAttributes:@{NSFontAttributeName: font}].width);
661
- CGFloat buttonWidth = MAX(44, textWidth + 16);
795
+ if (_textVariant) {
796
+ UIFont *activeFont = self.activeTabFont;
797
+ textWidth = MAX(textWidth, ceil([title sizeWithAttributes:@{NSFontAttributeName: activeFont}].width));
798
+ }
799
+ CGFloat buttonWidth = MAX(44, textWidth + (_textVariant ? 20 : 16));
662
800
  [widths addObject:@(buttonWidth)];
663
801
  itemsWidth += buttonWidth;
664
802
  }
665
803
  if (_buttons.count > 1) itemsWidth += (_buttons.count - 1) * _itemSpacing;
666
- CGFloat contentWidth = MAX(CGRectGetWidth(self.bounds),
804
+ CGFloat contentWidth = MAX(CGRectGetWidth(_scrollView.bounds),
667
805
  itemsWidth + _contentPaddingHorizontal * 2);
668
806
  _scrollView.contentSize = CGSizeMake(contentWidth, MAX(_barHeight, CGRectGetHeight(self.bounds)));
669
807
  [_indicatorFrames removeAllObjects];
@@ -693,14 +831,493 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
693
831
  [self updatePresentation];
694
832
  }
695
833
 
834
+ - (Class<RNCPagerNamedSVGBridge>)iconBridge {
835
+ Class cls = NSClassFromString(@"OneKeyNamedSVGBridge");
836
+ if (![cls respondsToSelector:@selector(cancelWithOwner:)] ||
837
+ ![cls respondsToSelector:@selector(requestWithOwner:name:color:width:height:scale:complete:)]) return Nil;
838
+ return (Class<RNCPagerNamedSVGBridge>)cls;
839
+ }
840
+ - (void)updateConfigJSON:(NSString *)json {
841
+ BOOL wasOwned = [self ownsSettledToolbar];
842
+ NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
843
+ id parsed = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : nil;
844
+ _config = [parsed isKindOfClass:NSDictionary.class] ? parsed : @{};
845
+ BOOL owned = [self ownsSettledToolbar];
846
+ if (owned != wasOwned) {
847
+ if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner];
848
+ _iconOwner = [NSObject new];
849
+ _iconRequestPending = NO;
850
+ _readyToolbarImage = nil;
851
+ _readyToolbarIdentity = nil;
852
+ _desiredToolbarKey = nil;
853
+ _desiredToolbarEntry = nil;
854
+ [self suspendToolbar];
855
+ if (!owned) {
856
+ ++_toolbarAnimationGeneration;
857
+ [_toolbarButton.layer removeAllAnimations];
858
+ _toolbarPresenceAnimating = NO;
859
+ _toolbarPresenceInterrupted = NO;
860
+ _toolbarButton.alpha = _toolbarVisible ? 1 : 0;
861
+ _toolbarButton.hidden = !_toolbarVisible;
862
+ [self setToolbarInteractive:_toolbarVisible];
863
+ }
864
+ }
865
+ if (!owned) {
866
+ _toolbarTouchKey = nil;
867
+ _toolbarButton.highlighted = NO;
868
+ _toolbarButton.hovering = NO;
869
+ }
870
+ _toolbarButton.adjustsImageWhenDisabled = !owned;
871
+ NSString *key = _config[@"selectedKey"];
872
+ if (![_config[@"controlled"] boolValue]) {
873
+ _progress = RNCClamp(_nativeProgress, 0, MAX(0, (NSInteger)_items.count - 1));
874
+ key = _items.count ? _items[(NSInteger)round(_progress)][@"key"] : nil;
875
+ }
876
+ if (_hasNativeProgress) {
877
+ [self setProgress:_nativeProgress];
878
+ } else {
879
+ NSUInteger index = [_items indexOfObjectPassingTest:^BOOL(NSDictionary *item, NSUInteger i, BOOL *stop) { return [item[@"key"] isEqual:key]; }];
880
+ if (index != NSNotFound) {
881
+ _progress = index;
882
+ _progressSource = index;
883
+ _progressDestination = index;
884
+ _progressFraction = 0;
885
+ }
886
+ }
887
+ [self updateToolbarForKey:key];
888
+ [self setNeedsLayout];
889
+ }
890
+ - (BOOL)usesControlledTextProgress { return _textVariant && [_config[@"controlled"] boolValue]; }
891
+ - (void)setLegacyToolbarVisible:(BOOL)visible {
892
+ _toolbarButton.enabled = visible;
893
+ _toolbarButton.userInteractionEnabled = visible;
894
+ _toolbarButton.accessibilityElementsHidden = !visible;
895
+ _toolbarButton.isAccessibilityElement = visible;
896
+ if (_toolbarVisible == visible) return;
897
+ _toolbarVisible = visible;
898
+ NSUInteger generation = ++_toolbarAnimationGeneration;
899
+ CALayer *presentation = _toolbarButton.layer.presentationLayer;
900
+ CGFloat alpha = presentation ? presentation.opacity : _toolbarButton.alpha;
901
+ [_toolbarButton.layer removeAllAnimations];
902
+ _toolbarButton.alpha = alpha;
903
+ _toolbarButton.enabled = visible;
904
+ _toolbarButton.userInteractionEnabled = visible;
905
+ _toolbarButton.accessibilityElementsHidden = !visible;
906
+ if (visible) _toolbarButton.hidden = NO;
907
+ if (self.window == nil) {
908
+ _toolbarButton.alpha = visible ? 1 : 0;
909
+ _toolbarButton.hidden = !visible;
910
+ return;
911
+ }
912
+ [UIView animateWithDuration:0.15 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
913
+ self->_toolbarButton.alpha = visible ? 1 : 0;
914
+ } completion:^(BOOL finished) {
915
+ if (self->_toolbarAnimationGeneration == generation && !visible) {
916
+ self->_toolbarButton.hidden = YES;
917
+ [self applyReadyLegacyToolbarImage];
918
+ }
919
+ }];
920
+ }
921
+ - (void)applyReadyLegacyToolbarImage {
922
+ if (_readyToolbarImage == nil || ![_readyToolbarIdentity isEqualToString:_iconIdentity]) return;
923
+ CALayer *presentation = _toolbarButton.layer.presentationLayer;
924
+ if (_toolbarButton.alpha > 0.001 || (presentation && presentation.opacity > 0.001)) return;
925
+ [_toolbarButton setImage:_readyToolbarImage forState:UIControlStateNormal];
926
+ _loadedIconIdentity = _readyToolbarIdentity;
927
+ _loadedToolbarImage = _readyToolbarImage;
928
+ _readyToolbarImage = nil;
929
+ _readyToolbarIdentity = nil;
930
+ _iconRequestPending = NO;
931
+ [self setLegacyToolbarVisible:YES];
932
+ }
933
+ - (void)updateLegacyToolbarForKey:(NSString *)key {
934
+ NSDictionary *toolbar = [_config[@"toolbar"] isKindOfClass:NSDictionary.class] ? _config[@"toolbar"] : nil;
935
+ _toolbarReservedWidth = toolbar == nil ? 0 : 44;
936
+ if (toolbar.count > 0) {
937
+ _toolbarReservedWidth = 0;
938
+ for (id value in toolbar.allValues) {
939
+ NSDictionary *entry = [value isKindOfClass:NSDictionary.class] ? value : @{};
940
+ CGFloat size = RNCPagerToolbarDimension(entry, @"size", 38, 1, 128);
941
+ CGFloat content = RNCPagerToolbarDimension(entry, @"contentWidth", MIN(24, size), 0, size);
942
+ CGFloat trailing = RNCPagerToolbarDimension(entry, @"trailingInset", 20, 0, 128);
943
+ _toolbarReservedWidth = MAX(_toolbarReservedWidth, content + trailing);
944
+ }
945
+ }
946
+ NSDictionary *entry = [toolbar[key ?: @""] isKindOfClass:NSDictionary.class] ? toolbar[key] : nil;
947
+ NSString *name = [entry[@"iconName"] isKindOfClass:NSString.class] ? entry[@"iconName"] : nil;
948
+ if (![_toolbarKey isEqualToString:key]) {
949
+ _toolbarTouchKey = nil;
950
+ _toolbarButton.highlighted = NO;
951
+ _toolbarButton.hovering = NO;
952
+ }
953
+ _toolbarKey = [key copy];
954
+ if (name.length == 0) {
955
+ _toolbarTouchKey = nil;
956
+ _toolbarButton.accessibilityLabel = nil;
957
+ _toolbarButton.accessibilityIdentifier = nil;
958
+ if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner];
959
+ _iconOwner = [NSObject new];
960
+ _iconRequestPending = NO;
961
+ _readyToolbarImage = nil;
962
+ _readyToolbarIdentity = nil;
963
+ [self setLegacyToolbarVisible:NO];
964
+ return;
965
+ }
966
+ _toolbarSize = RNCPagerToolbarDimension(entry, @"size", 38, 1, 128);
967
+ _toolbarIconSize = RNCPagerToolbarDimension(entry, @"iconSize", MIN(24, _toolbarSize), 1, _toolbarSize);
968
+ _toolbarContentWidth = RNCPagerToolbarDimension(entry, @"contentWidth", MIN(24, _toolbarSize), 0, _toolbarSize);
969
+ _toolbarTrailingInset = RNCPagerToolbarDimension(entry, @"trailingInset", 20, 0, 128);
970
+ _toolbarButton.layer.cornerRadius = _toolbarSize / 2;
971
+ _toolbarButton.iconSize = _toolbarIconSize;
972
+ [_toolbarButton setNeedsLayout];
973
+ _toolbarButton.baseColor = [entry[@"selected"] boolValue] ? ([RCTConvert UIColor:entry[@"selectedBackgroundColor"]] ?: UIColor.clearColor) : UIColor.clearColor;
974
+ _toolbarButton.hoverColor = [RCTConvert UIColor:entry[@"hoverBackgroundColor"]] ?: _toolbarButton.baseColor;
975
+ _toolbarButton.pressedColor = [RCTConvert UIColor:entry[@"pressedBackgroundColor"]] ?: _toolbarButton.baseColor;
976
+ [_toolbarButton refreshBackground];
977
+ _toolbarButton.accessibilityLabel = entry[@"accessibilityLabel"];
978
+ _toolbarButton.accessibilityIdentifier = entry[@"testID"];
979
+ NSString *color = [entry[@"iconColor"] isKindOfClass:NSString.class] ? entry[@"iconColor"] : @"#000000";
980
+ CGFloat scale = self.window.screen.scale ?: UIScreen.mainScreen.scale;
981
+ NSString *identity = [NSString stringWithFormat:@"%@|%@|%g|%g", name, color, scale, _toolbarIconSize];
982
+ if ([_loadedIconIdentity isEqualToString:identity] && [_toolbarButton imageForState:UIControlStateNormal] != nil) {
983
+ if (_iconRequestPending) {
984
+ [[self iconBridge] cancelWithOwner:_iconOwner];
985
+ _iconOwner = [NSObject new];
986
+ }
987
+ _iconIdentity = identity;
988
+ _iconRequestPending = NO;
989
+ _readyToolbarImage = nil;
990
+ _readyToolbarIdentity = nil;
991
+ [self setLegacyToolbarVisible:YES];
992
+ return;
993
+ }
994
+ if ((_iconRequestPending || [_failedIconIdentity isEqualToString:identity]) && [_iconIdentity isEqualToString:identity]) return;
995
+ if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner];
996
+ NSObject *owner = [NSObject new];
997
+ _iconOwner = owner;
998
+ _iconIdentity = identity;
999
+ _iconRequestPending = NO;
1000
+ _readyToolbarImage = nil;
1001
+ _readyToolbarIdentity = nil;
1002
+ [self setLegacyToolbarVisible:NO];
1003
+ if ([_failedIconIdentity isEqualToString:identity] || self.window == nil) return;
1004
+ _iconRequestPending = YES;
1005
+ __weak __typeof__(self) weakSelf = self;
1006
+ [[self iconBridge] requestWithOwner:owner name:name color:color width:(NSInteger)ceil(_toolbarIconSize * scale) height:(NSInteger)ceil(_toolbarIconSize * scale) scale:scale complete:^(UIImage *image) {
1007
+ __strong __typeof__(weakSelf) self = weakSelf;
1008
+ if (!self || self->_iconOwner != owner || ![self->_iconIdentity isEqualToString:identity] || !self.window) return;
1009
+ if (image == nil) {
1010
+ self->_iconRequestPending = NO;
1011
+ self->_failedIconIdentity = identity;
1012
+ return;
1013
+ }
1014
+ UIImage *scaled = image.CGImage ? [UIImage imageWithCGImage:image.CGImage scale:scale orientation:UIImageOrientationUp] : image;
1015
+ self->_readyToolbarImage = [scaled imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
1016
+ self->_readyToolbarIdentity = identity;
1017
+ [self applyReadyLegacyToolbarImage];
1018
+ }];
1019
+ }
1020
+
1021
+ - (BOOL)ownsSettledToolbar {
1022
+ return [_config[@"controlled"] boolValue] && [_config[@"toolbar"] isKindOfClass:NSDictionary.class];
1023
+ }
1024
+ - (BOOL)toolbarCanCommit {
1025
+ if (!self.window) return NO;
1026
+ if (![self ownsSettledToolbar]) return YES;
1027
+ if (_toolbarPagerBusy || _toolbarPageIndex < 0) return NO;
1028
+ NSArray *pages = [_config[@"pageKeys"] isKindOfClass:NSArray.class] ? _config[@"pageKeys"] : @[];
1029
+ NSArray *items = [_items valueForKey:@"key"];
1030
+ BOOL valid = pages.count > 0 && [pages containsObject:_config[@"selectedKey"] ?: @""] &&
1031
+ [NSSet setWithArray:pages].count == pages.count && [NSSet setWithArray:items].count == items.count;
1032
+ for (id key in pages) if (![items containsObject:key]) { valid = NO; break; }
1033
+ return !valid || (_toolbarPageIndex < pages.count && [pages[_toolbarPageIndex] isEqual:_config[@"selectedKey"]]);
1034
+ }
1035
+ - (void)setToolbarInteractive:(BOOL)interactive {
1036
+ _toolbarButton.enabled = interactive;
1037
+ _toolbarButton.userInteractionEnabled = interactive;
1038
+ _toolbarButton.accessibilityElementsHidden = !interactive;
1039
+ _toolbarButton.isAccessibilityElement = interactive;
1040
+ if (!interactive) {
1041
+ _toolbarTouchKey = nil;
1042
+ _toolbarButton.highlighted = NO;
1043
+ _toolbarButton.hovering = NO;
1044
+ [_toolbarButton refreshBackground];
1045
+ }
1046
+ }
1047
+ - (void)cancelToolbarRequest {
1048
+ if (_iconRequestPending || _readyToolbarImage) {
1049
+ if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner];
1050
+ _iconOwner = [NSObject new];
1051
+ _iconRequestPending = NO;
1052
+ _readyToolbarImage = nil;
1053
+ _readyToolbarIdentity = nil;
1054
+ }
1055
+ }
1056
+ - (void)suspendToolbar {
1057
+ [self setToolbarInteractive:NO];
1058
+ [self cancelToolbarRequest];
1059
+ _desiredToolbarKey = nil;
1060
+ _desiredToolbarEntry = nil;
1061
+ if (_toolbarPresenceAnimating) {
1062
+ CALayer *presentation = _toolbarButton.layer.presentationLayer;
1063
+ CGFloat alpha = presentation ? presentation.opacity : _toolbarButton.alpha;
1064
+ ++_toolbarAnimationGeneration;
1065
+ [_toolbarButton.layer removeAllAnimations];
1066
+ _toolbarButton.alpha = alpha;
1067
+ _toolbarPresenceAnimating = NO;
1068
+ _toolbarPresenceInterrupted = YES;
1069
+ }
1070
+ }
1071
+ - (void)updateToolbarPagerIndex:(NSInteger)index busy:(BOOL)busy {
1072
+ if (_toolbarPagerBusy == busy && (busy || _toolbarPageIndex == index)) return;
1073
+ _toolbarPagerBusy = busy;
1074
+ if (!busy) _toolbarPageIndex = index;
1075
+ NSString *key = [_config[@"controlled"] boolValue] ? _config[@"selectedKey"] : _toolbarKey;
1076
+ [self updateToolbarForKey:key];
1077
+ }
1078
+ - (void)setToolbarVisible:(BOOL)visible {
1079
+ if (_toolbarVisible == visible && !_toolbarPresenceInterrupted) return;
1080
+ _toolbarVisible = visible;
1081
+ _toolbarPresenceInterrupted = NO;
1082
+ NSUInteger generation = ++_toolbarAnimationGeneration;
1083
+ CALayer *presentation = _toolbarButton.layer.presentationLayer;
1084
+ CGFloat alpha = presentation ? presentation.opacity : _toolbarButton.alpha;
1085
+ [_toolbarButton.layer removeAllAnimations];
1086
+ _toolbarButton.alpha = alpha;
1087
+ if (visible) _toolbarButton.hidden = NO;
1088
+ if (!self.window) {
1089
+ _toolbarPresenceAnimating = NO;
1090
+ _toolbarButton.alpha = visible ? 1 : 0;
1091
+ _toolbarButton.hidden = !visible;
1092
+ return;
1093
+ }
1094
+ _toolbarPresenceAnimating = YES;
1095
+ [UIView animateWithDuration:0.10 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
1096
+ self->_toolbarButton.alpha = visible ? 1 : 0;
1097
+ } completion:^(BOOL finished) {
1098
+ if (self->_toolbarAnimationGeneration != generation) return;
1099
+ self->_toolbarPresenceAnimating = NO;
1100
+ if (!visible) self->_toolbarButton.hidden = YES;
1101
+ }];
1102
+ }
1103
+ - (void)commitToolbarImage:(UIImage *)image identity:(NSString *)identity {
1104
+ if (![self toolbarCanCommit] || !_desiredToolbarKey || !_desiredToolbarEntry) return;
1105
+ NSDictionary *entry = _desiredToolbarEntry;
1106
+ BOOL changed = ![_toolbarKey isEqualToString:_desiredToolbarKey] || ![_committedToolbarEntry isEqual:entry] || _loadedToolbarImage != image;
1107
+ if (![_toolbarKey isEqualToString:_desiredToolbarKey]) [self setToolbarInteractive:NO];
1108
+ _toolbarKey = [_desiredToolbarKey copy];
1109
+ _committedToolbarEntry = entry;
1110
+ _toolbarSize = RNCPagerToolbarDimension(entry, @"size", 38, 1, 128);
1111
+ _toolbarIconSize = RNCPagerToolbarDimension(entry, @"iconSize", MIN(24, _toolbarSize), 1, _toolbarSize);
1112
+ _toolbarContentWidth = RNCPagerToolbarDimension(entry, @"contentWidth", MIN(24, _toolbarSize), 0, _toolbarSize);
1113
+ _toolbarTrailingInset = RNCPagerToolbarDimension(entry, @"trailingInset", 20, 0, 128);
1114
+ _toolbarButton.layer.cornerRadius = _toolbarSize / 2;
1115
+ _toolbarButton.iconSize = _toolbarIconSize;
1116
+ [_toolbarButton setNeedsLayout];
1117
+ _toolbarButton.baseColor = [entry[@"selected"] boolValue] ? ([RCTConvert UIColor:entry[@"selectedBackgroundColor"]] ?: UIColor.clearColor) : UIColor.clearColor;
1118
+ _toolbarButton.hoverColor = [RCTConvert UIColor:entry[@"hoverBackgroundColor"]] ?: _toolbarButton.baseColor;
1119
+ _toolbarButton.pressedColor = [RCTConvert UIColor:entry[@"pressedBackgroundColor"]] ?: _toolbarButton.baseColor;
1120
+ [_toolbarButton refreshBackground];
1121
+ _toolbarButton.accessibilityLabel = entry[@"accessibilityLabel"];
1122
+ _toolbarButton.accessibilityIdentifier = entry[@"testID"];
1123
+ [_toolbarButton setImage:image forState:UIControlStateNormal];
1124
+ _loadedIconIdentity = identity;
1125
+ _loadedToolbarImage = image;
1126
+ _readyToolbarImage = nil;
1127
+ _readyToolbarIdentity = nil;
1128
+ _iconRequestPending = NO;
1129
+ [self setToolbarVisible:YES];
1130
+ [self setToolbarInteractive:YES];
1131
+ if (changed) [self setNeedsLayout];
1132
+ }
1133
+ - (void)applyReadyToolbarImage {
1134
+ if (!_readyToolbarImage || ![_readyToolbarIdentity isEqualToString:_iconIdentity] || ![self toolbarCanCommit]) return;
1135
+ [self commitToolbarImage:_readyToolbarImage identity:_readyToolbarIdentity];
1136
+ }
1137
+ - (void)hideFailedToolbar {
1138
+ [self setToolbarInteractive:NO];
1139
+ _toolbarKey = nil;
1140
+ _committedToolbarEntry = nil;
1141
+ [_toolbarButton setImage:nil forState:UIControlStateNormal];
1142
+ _toolbarButton.accessibilityLabel = nil;
1143
+ _toolbarButton.accessibilityIdentifier = nil;
1144
+ [self setToolbarVisible:NO];
1145
+ }
1146
+ - (void)updateToolbarForKey:(NSString *)key {
1147
+ if (![self ownsSettledToolbar]) { [self updateLegacyToolbarForKey:key]; return; }
1148
+ NSDictionary *toolbar = [_config[@"toolbar"] isKindOfClass:NSDictionary.class] ? _config[@"toolbar"] : nil;
1149
+ _toolbarReservedWidth = toolbar == nil ? 0 : 44;
1150
+ if (toolbar.count > 0) {
1151
+ _toolbarReservedWidth = 0;
1152
+ for (id value in toolbar.allValues) {
1153
+ NSDictionary *entry = [value isKindOfClass:NSDictionary.class] ? value : @{};
1154
+ CGFloat size = RNCPagerToolbarDimension(entry, @"size", 38, 1, 128);
1155
+ CGFloat content = RNCPagerToolbarDimension(entry, @"contentWidth", MIN(24, size), 0, size);
1156
+ CGFloat trailing = RNCPagerToolbarDimension(entry, @"trailingInset", 20, 0, 128);
1157
+ _toolbarReservedWidth = MAX(_toolbarReservedWidth, content + trailing);
1158
+ }
1159
+ }
1160
+ if (![self toolbarCanCommit]) { [self suspendToolbar]; return; }
1161
+ NSDictionary *entry = [toolbar[key ?: @""] isKindOfClass:NSDictionary.class] ? toolbar[key] : nil;
1162
+ NSString *name = [entry[@"iconName"] isKindOfClass:NSString.class] ? entry[@"iconName"] : nil;
1163
+ if (!name.length) {
1164
+ [self cancelToolbarRequest];
1165
+ _desiredToolbarKey = nil;
1166
+ _desiredToolbarEntry = nil;
1167
+ _toolbarKey = nil;
1168
+ _committedToolbarEntry = nil;
1169
+ [self setToolbarInteractive:NO];
1170
+ _toolbarButton.accessibilityLabel = nil;
1171
+ _toolbarButton.accessibilityIdentifier = nil;
1172
+ [self setToolbarVisible:NO];
1173
+ return;
1174
+ }
1175
+ _desiredToolbarKey = [key copy];
1176
+ _desiredToolbarEntry = [entry copy];
1177
+ CGFloat size = RNCPagerToolbarDimension(entry, @"size", 38, 1, 128);
1178
+ CGFloat iconSize = RNCPagerToolbarDimension(entry, @"iconSize", MIN(24, size), 1, size);
1179
+ NSString *color = [entry[@"iconColor"] isKindOfClass:NSString.class] ? entry[@"iconColor"] : @"#000000";
1180
+ CGFloat scale = self.window.screen.scale ?: UIScreen.mainScreen.scale;
1181
+ NSString *identity = [NSString stringWithFormat:@"%@|%@|%g|%g", name, color, scale, iconSize];
1182
+ if ([_loadedIconIdentity isEqualToString:identity] && _loadedToolbarImage) {
1183
+ [self cancelToolbarRequest];
1184
+ _iconIdentity = identity;
1185
+ [self commitToolbarImage:_loadedToolbarImage identity:identity];
1186
+ return;
1187
+ }
1188
+ if ([_iconIdentity isEqualToString:identity] && (_iconRequestPending || _readyToolbarImage)) {
1189
+ [self setToolbarInteractive:NO];
1190
+ [self applyReadyToolbarImage];
1191
+ return;
1192
+ }
1193
+ [self cancelToolbarRequest];
1194
+ _iconIdentity = identity;
1195
+ [self setToolbarInteractive:NO];
1196
+ if ([_failedIconIdentity isEqualToString:identity]) { [self hideFailedToolbar]; return; }
1197
+ NSObject *owner = [NSObject new];
1198
+ _iconOwner = owner;
1199
+ _iconRequestPending = YES;
1200
+ __weak __typeof__(self) weakSelf = self;
1201
+ [[self iconBridge] requestWithOwner:owner name:name color:color width:(NSInteger)ceil(iconSize * scale) height:(NSInteger)ceil(iconSize * scale) scale:scale complete:^(UIImage *image) {
1202
+ __strong __typeof__(weakSelf) self = weakSelf;
1203
+ if (!self || self->_iconOwner != owner || ![self->_iconIdentity isEqualToString:identity] || !self.window) return;
1204
+ self->_iconRequestPending = NO;
1205
+ if (!image) {
1206
+ self->_failedIconIdentity = identity;
1207
+ if ([self toolbarCanCommit]) [self hideFailedToolbar];
1208
+ return;
1209
+ }
1210
+ UIImage *scaled = image.CGImage ? [UIImage imageWithCGImage:image.CGImage scale:scale orientation:UIImageOrientationUp] : image;
1211
+ self->_readyToolbarImage = [scaled imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
1212
+ self->_readyToolbarIdentity = identity;
1213
+ [self applyReadyToolbarImage];
1214
+ }];
1215
+ }
1216
+ - (void)didMoveToWindow {
1217
+ [super didMoveToWindow];
1218
+ if (![self ownsSettledToolbar]) {
1219
+ if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner];
1220
+ _iconIdentity = nil;
1221
+ _loadedIconIdentity = nil;
1222
+ _iconRequestPending = NO;
1223
+ _readyToolbarImage = nil;
1224
+ _readyToolbarIdentity = nil;
1225
+ _failedIconIdentity = nil;
1226
+ _iconOwner = [NSObject new];
1227
+ _toolbarAnimationGeneration++;
1228
+ [_toolbarButton.layer removeAllAnimations];
1229
+ _toolbarVisible = NO;
1230
+ _toolbarButton.alpha = 0;
1231
+ _toolbarButton.hidden = YES;
1232
+ _toolbarButton.enabled = NO;
1233
+ _toolbarButton.userInteractionEnabled = NO;
1234
+ _toolbarButton.accessibilityElementsHidden = YES;
1235
+ _toolbarTouchKey = nil;
1236
+ _toolbarTouchStarted = NO;
1237
+ _toolbarButton.highlighted = NO;
1238
+ _toolbarButton.hovering = NO;
1239
+ [self updateToolbarForKey:_toolbarKey];
1240
+ return;
1241
+ }
1242
+ [self suspendToolbar];
1243
+ _toolbarPagerBusy = YES;
1244
+ _toolbarPageIndex = -1;
1245
+ _toolbarTouchStarted = NO;
1246
+ if (self.window && self.onToolbarNeedsSettlement) self.onToolbarNeedsSettlement();
1247
+ }
1248
+ - (void)dealloc { if (_iconOwner) [[self iconBridge] cancelWithOwner:_iconOwner]; }
1249
+ - (void)toolbarTouchDown {
1250
+ _toolbarTouchStarted = YES;
1251
+ _toolbarTouchKey = _toolbarButton.enabled && [self toolbarCanCommit] ? [_toolbarKey copy] : nil;
1252
+ }
1253
+ - (void)toolbarCancel {
1254
+ _toolbarTouchKey = nil;
1255
+ if (![self ownsSettledToolbar]) _toolbarTouchStarted = NO;
1256
+ }
1257
+ - (void)toolbarPress:(UIControl *)sender forEvent:(UIEvent *)event {
1258
+ if (![self ownsSettledToolbar]) {
1259
+ NSString *key = _toolbarTouchStarted ? _toolbarTouchKey : _toolbarKey;
1260
+ _toolbarTouchStarted = NO; _toolbarTouchKey = nil;
1261
+ if (key && [key isEqualToString:_toolbarKey] && !_toolbarButton.hidden && _toolbarButton.enabled && self.window && self.onToolbarPress)
1262
+ self.onToolbarPress(key, [_toolbarButton convertRect:_toolbarButton.bounds toView:self.window]);
1263
+ return;
1264
+ }
1265
+ BOOL pointer = event && event.type == UIEventTypeTouches;
1266
+ NSString *key = pointer ? (_toolbarTouchStarted ? _toolbarTouchKey : nil) : _toolbarKey;
1267
+ if (pointer) { _toolbarTouchStarted = NO; _toolbarTouchKey = nil; }
1268
+ BOOL selectedMatches = ![_config[@"controlled"] boolValue] || [key isEqual:_config[@"selectedKey"]];
1269
+ if (key && selectedMatches && [key isEqualToString:_toolbarKey] && [self toolbarCanCommit] &&
1270
+ !_toolbarButton.hidden && _toolbarButton.enabled && self.onToolbarPress) {
1271
+ self.onToolbarPress(key, [_toolbarButton convertRect:_toolbarButton.bounds toView:self.window]);
1272
+ }
1273
+ }
1274
+
696
1275
  - (void)setProgress:(CGFloat)progress
697
1276
  {
1277
+ _hasMappedPageProgress = NO;
1278
+ _nativeProgress = progress;
1279
+ _hasNativeProgress = YES;
698
1280
  if (_items.count == 0) return;
699
1281
  if (fabs(_progress - progress) >= 0.001) _shouldCenterSelectedItem = YES;
1282
+ if ([_config[@"controlled"] boolValue]) {
1283
+ NSArray *pageKeys = [_config[@"pageKeys"] isKindOfClass:NSArray.class] ? _config[@"pageKeys"] : @[];
1284
+ NSMutableArray<NSNumber *> *indices = [NSMutableArray new];
1285
+ NSSet *unique = [NSSet setWithArray:pageKeys];
1286
+ NSArray *itemKeys = [_items valueForKey:@"key"];
1287
+ BOOL valid = pageKeys.count > 0 && [pageKeys containsObject:_config[@"selectedKey"] ?: @""] && unique.count == pageKeys.count && [NSSet setWithArray:itemKeys].count == itemKeys.count;
1288
+ for (NSString *key in pageKeys) {
1289
+ NSUInteger index = [_items indexOfObjectPassingTest:^BOOL(NSDictionary *item, NSUInteger i, BOOL *stop) { return [item[@"key"] isEqual:key]; }];
1290
+ if (index == NSNotFound) { valid = NO; break; }
1291
+ [indices addObject:@(index)];
1292
+ }
1293
+ if (!valid) {
1294
+ NSUInteger selected = [_items indexOfObjectPassingTest:^BOOL(NSDictionary *item, NSUInteger i, BOOL *stop) { return [item[@"key"] isEqual:self->_config[@"selectedKey"]]; }];
1295
+ if (selected == NSNotFound) return;
1296
+ _progressSource = selected; _progressDestination = selected; _progressFraction = 0;
1297
+ _progress = selected;
1298
+ if (_indicatorFrames.count == _items.count) [self updatePresentation];
1299
+ return;
1300
+ }
1301
+ _hasMappedPageProgress = YES;
1302
+ CGFloat bounded = RNCClamp(progress, 0, pageKeys.count - 1);
1303
+ NSInteger lower = (NSInteger)floor(bounded);
1304
+ NSInteger upper = MIN(lower + 1, pageKeys.count - 1);
1305
+ _progressSource = indices[lower].integerValue;
1306
+ _progressDestination = indices[upper].integerValue;
1307
+ _progressFraction = bounded - lower;
1308
+ progress = _progressSource + (_progressDestination - _progressSource) * _progressFraction;
1309
+ }
700
1310
  _progress = RNCClamp(progress, 0, _items.count - 1);
701
1311
  if (_indicatorFrames.count == _items.count) [self updatePresentation];
702
1312
  }
703
1313
 
1314
+ - (NSInteger)visualFontOwnerIndex:(NSInteger)semanticIndex {
1315
+ if (_textVariant && [_config[@"controlled"] boolValue] && _hasMappedPageProgress) {
1316
+ return _progressFraction < 0.5 ? _progressSource : _progressDestination;
1317
+ }
1318
+ return semanticIndex;
1319
+ }
1320
+
704
1321
  - (void)updatePresentation
705
1322
  {
706
1323
  NSInteger count = _buttons.count;
@@ -718,11 +1335,22 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
718
1335
  _segmented ? CGRectGetHeight(fromFrame) : _indicatorHeight
719
1336
  );
720
1337
  _indicatorView.frame = indicatorFrame;
721
- _indicatorView.hidden = CGRectGetHeight(indicatorFrame) <= 0;
1338
+ _indicatorView.hidden = _textVariant || CGRectGetHeight(indicatorFrame) <= 0;
722
1339
 
723
1340
  NSInteger selectedIndex = (NSInteger)round(progress);
1341
+ if ([_config[@"controlled"] boolValue]) {
1342
+ NSUInteger selected = [_items indexOfObjectPassingTest:^BOOL(NSDictionary *item, NSUInteger i, BOOL *stop) { return [item[@"key"] isEqual:self->_config[@"selectedKey"]]; }];
1343
+ selectedIndex = selected == NSNotFound ? -1 : (NSInteger)selected;
1344
+ }
1345
+ NSInteger fontOwnerIndex = [self visualFontOwnerIndex:selectedIndex];
1346
+ NSString *key = selectedIndex >= 0 && selectedIndex < count ? _items[selectedIndex][@"key"] : nil;
1347
+ [self updateToolbarForKey:key];
724
1348
  [_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
725
1349
  CGFloat emphasis = 1 - MIN(1, fabs(progress - index));
1350
+ if ([self->_config[@"controlled"] boolValue]) {
1351
+ emphasis = index == self->_progressSource ? 1 - self->_progressFraction : 0;
1352
+ if (index == self->_progressDestination) emphasis += self->_progressFraction;
1353
+ }
726
1354
  UIColor *color = RNCInterpolateColor(
727
1355
  self->_inactiveTextColor,
728
1356
  self->_activeTextColor,
@@ -730,6 +1358,11 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
730
1358
  self.traitCollection
731
1359
  );
732
1360
  [button setTitleColor:color forState:UIControlStateNormal];
1361
+ if (self->_textVariant) {
1362
+ button.titleLabel.font = index == fontOwnerIndex
1363
+ ? self.activeTabFont
1364
+ : self.tabFont;
1365
+ }
733
1366
  button.accessibilityTraits = index == selectedIndex
734
1367
  ? UIAccessibilityTraitButton | UIAccessibilityTraitSelected
735
1368
  : UIAccessibilityTraitButton;
@@ -1534,6 +2167,24 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1534
2167
  - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)other { return YES; }
1535
2168
  @end
1536
2169
 
2170
+ @protocol RNCPagerPresentationProgressTarget <NSObject>
2171
+ - (void)sampleProgrammaticTabProgress:(CADisplayLink *)link;
2172
+ - (void)sampleToolbarSettlement:(CADisplayLink *)link;
2173
+ @end
2174
+
2175
+ @interface RNCPagerPresentationProgressProxy : NSObject
2176
+ @property (nonatomic, weak) id<RNCPagerPresentationProgressTarget> target;
2177
+ @property (nonatomic, assign) BOOL toolbarSettlement;
2178
+ @end
2179
+ @implementation RNCPagerPresentationProgressProxy
2180
+ - (void)tick:(CADisplayLink *)link {
2181
+ id<RNCPagerPresentationProgressTarget> target = self.target;
2182
+ if (target && self.toolbarSettlement) [target sampleToolbarSettlement:link];
2183
+ else if (target) [target sampleProgrammaticTabProgress:link];
2184
+ else [link invalidate];
2185
+ }
2186
+ @end
2187
+
1537
2188
  @interface RNCCollapsiblePagerViewComponentView () <
1538
2189
  RCTRNCCollapsiblePagerViewViewProtocol,
1539
2190
  UIPageViewControllerDataSource,
@@ -1542,8 +2193,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1542
2193
  UIGestureRecognizerDelegate,
1543
2194
  RCTMountingTransactionObserving,
1544
2195
  RNCNativeScrollerInsetCoordinator,
1545
- RNCPagerViewNestedOverlayObserver
2196
+ RNCPagerViewNestedOverlayObserver,
2197
+ RNCPagerPresentationProgressTarget
1546
2198
  >
2199
+ - (void)syncNativeToolbarState;
2200
+ - (void)cancelNativeToolbarSettlement;
2201
+ - (void)scheduleNativeToolbarSettlement;
1547
2202
  - (void)finishPagerScrollEmittingSelection:(BOOL)emitSelection;
1548
2203
  - (void)cancelPendingNativePagerScroll;
1549
2204
  - (void)completeTransitionOnNextRunLoopForGeneration:(NSUInteger)generation
@@ -1662,6 +2317,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1662
2317
  BOOL _nativeSmoothHeaderScrollEnabled;
1663
2318
  UIPanGestureRecognizer *_blockerGesture;
1664
2319
  BOOL _transitioning;
2320
+ BOOL _toolbarTransitionPending;
2321
+ CADisplayLink *_toolbarSettleDisplayLink;
2322
+ NSUInteger _toolbarSettleGeneration;
2323
+ NSUInteger _toolbarSettleTransition;
2324
+ NSInteger _toolbarSettlePage;
2325
+ CGFloat _toolbarSettleWidth;
2326
+ NSUInteger _toolbarSettleTicks;
2327
+ BOOL _toolbarSettleAttempted;
1665
2328
  BOOL _isPagerDragging;
1666
2329
  BOOL _directNativePagerEnabled;
1667
2330
  BOOL _pendingDirectNativePagerEnabled;
@@ -1672,6 +2335,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1672
2335
  NSUInteger _pendingNativePagerScrollGeneration;
1673
2336
  NSUInteger _pendingNativePagerScrollTransition;
1674
2337
  CGFloat _pendingNativePagerScrollTarget;
2338
+ CADisplayLink *_tabProgressDisplayLink;
2339
+ NSUInteger _tabProgressGeneration;
2340
+ NSUInteger _tabProgressTransition;
2341
+ CGFloat _tabProgressWidth;
1675
2342
  NSIndexSet *_programmaticPageMaskIndexes;
1676
2343
  NSMapTable<UIScrollView *, RNCCollapsiblePagerTransitionEdgeOwner *> *_programmaticScrollEdges;
1677
2344
  NSMutableArray<RNCCollapsiblePagerTransitionEdgeOwner *> *_programmaticScrollEdgeOwners;
@@ -1851,6 +2518,23 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1851
2518
  });
1852
2519
  }
1853
2520
  };
2521
+ _nativeTabBarView.onToolbarNeedsSettlement = ^{
2522
+ __strong __typeof__(weakSelf) self = weakSelf;
2523
+ if (!self) return;
2524
+ NSUInteger generation = self->_generation;
2525
+ dispatch_async(dispatch_get_main_queue(), ^{
2526
+ if (self->_generation == generation) [self syncNativeToolbarState];
2527
+ });
2528
+ };
2529
+ _nativeTabBarView.onToolbarPress = ^(NSString *key, CGRect rect) {
2530
+ __strong __typeof__(weakSelf) self = weakSelf;
2531
+ if (!self || self->_isBeingRecycled) return;
2532
+ const auto emitter = [self eventEmitter];
2533
+ if (emitter) emitter->onNativeToolbarPress({
2534
+ .key = std::string(key.UTF8String ?: ""),
2535
+ .anchorRect = {.x = rect.origin.x, .y = rect.origin.y, .width = rect.size.width, .height = rect.size.height}
2536
+ });
2537
+ };
1854
2538
  [_sharedHeaderHostView addSubview:_nativeTabBarView];
1855
2539
 
1856
2540
  _nativeSubHeaderView = [RNCCollapsiblePagerNativeSubHeaderView new];
@@ -1935,11 +2619,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1935
2619
  - (void)didMoveToWindow
1936
2620
  {
1937
2621
  [super didMoveToWindow];
2622
+ [self cancelNativeToolbarSettlement];
2623
+ if (self.window == nil) [self stopProgrammaticTabProgress];
1938
2624
  if (self.window == nil && _pendingNativePagerScrollCompletion != nil) {
1939
2625
  [self cancelPendingNativePagerScroll];
1940
2626
  _pagerScrollView.scrollEnabled = _scrollEnabled;
1941
2627
  [self finishPagerScrollEmittingSelection:NO];
1942
2628
  }
2629
+ [self syncNativeToolbarState];
1943
2630
  [self registerExternalOuterPager];
1944
2631
  BOOL externalHeightChanged = [self resolveExternalTopOverlay];
1945
2632
  if (self.window == nil) [self restoreScrollEdgeContentMasks];
@@ -2054,6 +2741,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2054
2741
  BOOL wasTransitioning = _externalOuterTransitioning;
2055
2742
  _externalOuterPageActive = active;
2056
2743
  _externalOuterTransitioning = transitioning;
2744
+ [self syncNativeToolbarState];
2057
2745
  if (transitioning && !wasTransitioning) {
2058
2746
  [self updateTopScrollEdgeForScrollView:nil];
2059
2747
  [self liftSharedHeadersForPagerTransition:@"outer"];
@@ -2418,8 +3106,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2418
3106
  [host addSubview:marker];
2419
3107
  return marker;
2420
3108
  };
2421
- owner.ownsTransitionHosts = allowExternalOverlay && _topOverlayView == nil &&
2422
- _externalTopOverlayTag > 0 && pageIndex != _currentIndex;
3109
+ owner.ownsTransitionHosts = pageIndex != _currentIndex;
2423
3110
  UIView *(^transitionHost)(UIView *) = ^UIView *(UIView *referenceHost) {
2424
3111
  CGRect frame = [referenceHost convertRect:referenceHost.bounds toView:owner.page];
2425
3112
  CGRect viewport = [scrollView convertRect:scrollView.bounds toView:owner.page];
@@ -2680,6 +3367,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2680
3367
 
2681
3368
  - (void)dealloc
2682
3369
  {
3370
+ [self cancelNativeToolbarSettlement];
3371
+ [self stopProgrammaticTabProgress];
2683
3372
  [self clearProgrammaticScrollEdges];
2684
3373
  [self updateTopScrollEdgeForScrollView:nil];
2685
3374
  [self cancelHeaderSettledCheck];
@@ -2734,6 +3423,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2734
3423
  }
2735
3424
  _hasPendingDirectNativePagerChange = NO;
2736
3425
  if (_directNativePagerEnabled == enabled) return;
3426
+ [self cancelNativeToolbarSettlement];
2737
3427
  [self cancelPendingNativePagerScroll];
2738
3428
 
2739
3429
  [self restoreScrollEdgeContentMasks];
@@ -2796,12 +3486,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2796
3486
  return width * MAX(0, physicalIndex);
2797
3487
  }
2798
3488
 
2799
- - (CGFloat)directPagerProgress
3489
+ - (CGFloat)directPagerProgressForOffset:(CGFloat)offset
2800
3490
  {
2801
3491
  CGFloat width = CGRectGetWidth(_pagerScrollView.bounds);
2802
3492
  if (width <= 0 || _pageControllers.count == 0) return _currentIndex;
2803
3493
  CGFloat physicalProgress = RNCClamp(
2804
- _pagerScrollView.contentOffset.x / width,
3494
+ offset / width,
2805
3495
  0,
2806
3496
  _pageControllers.count - 1
2807
3497
  );
@@ -2810,6 +3500,50 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2810
3500
  : _pageControllers.count - 1 - physicalProgress;
2811
3501
  }
2812
3502
 
3503
+ - (CGFloat)directPagerProgress {
3504
+ return [self directPagerProgressForOffset:_pagerScrollView.contentOffset.x];
3505
+ }
3506
+
3507
+ - (void)stopProgrammaticTabProgress {
3508
+ if (_tabProgressDisplayLink != nil && _tabProgressWidth > 0 && fabs(CGRectGetWidth(_pagerScrollView.bounds) - _tabProgressWidth) < 0.5) {
3509
+ CALayer *presentation = _pagerScrollView.layer.presentationLayer;
3510
+ if (presentation && std::isfinite(presentation.bounds.origin.x)) {
3511
+ [_nativeTabBarView setProgress:[self directPagerProgressForOffset:presentation.bounds.origin.x]];
3512
+ }
3513
+ }
3514
+ [_tabProgressDisplayLink invalidate];
3515
+ _tabProgressDisplayLink = nil;
3516
+ }
3517
+
3518
+ - (void)updateNativeTabProgress:(CGFloat)progress {
3519
+ if (_tabProgressDisplayLink == nil) [_nativeTabBarView setProgress:progress];
3520
+ }
3521
+
3522
+ - (void)sampleProgrammaticTabProgress:(CADisplayLink *)link {
3523
+ if (link != _tabProgressDisplayLink) { [link invalidate]; return; }
3524
+ if (self.window == nil || !_transitioning || ![_nativeTabBarView usesControlledTextProgress] ||
3525
+ _tabProgressGeneration != _generation || _tabProgressTransition != _transitionId ||
3526
+ !std::isfinite(CGRectGetWidth(_pagerScrollView.bounds)) || _tabProgressWidth <= 0 ||
3527
+ fabs(CGRectGetWidth(_pagerScrollView.bounds) - _tabProgressWidth) >= 0.5) {
3528
+ [self stopProgrammaticTabProgress];
3529
+ return;
3530
+ }
3531
+ CALayer *presentation = _pagerScrollView.layer.presentationLayer;
3532
+ if (presentation == nil || !std::isfinite(presentation.bounds.origin.x)) return;
3533
+ [_nativeTabBarView setProgress:[self directPagerProgressForOffset:presentation.bounds.origin.x]];
3534
+ }
3535
+
3536
+ - (void)startProgrammaticTabProgress {
3537
+ [self stopProgrammaticTabProgress];
3538
+ RNCPagerPresentationProgressProxy *proxy = [RNCPagerPresentationProgressProxy new];
3539
+ proxy.target = self;
3540
+ _tabProgressWidth = CGRectGetWidth(_pagerScrollView.bounds);
3541
+ _tabProgressGeneration = _generation;
3542
+ _tabProgressTransition = _transitionId;
3543
+ _tabProgressDisplayLink = [CADisplayLink displayLinkWithTarget:proxy selector:@selector(tick:)];
3544
+ [_tabProgressDisplayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
3545
+ }
3546
+
2813
3547
  #pragma mark - Smooth shared header ownership
2814
3548
 
2815
3549
  - (void)restorePinnedStickyHeader
@@ -3068,8 +3802,81 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3068
3802
  }
3069
3803
  }
3070
3804
 
3805
+ - (void)cancelNativeToolbarSettlement {
3806
+ [_toolbarSettleDisplayLink invalidate];
3807
+ _toolbarSettleDisplayLink = nil;
3808
+ _toolbarSettleAttempted = NO;
3809
+ _toolbarSettleTicks = 0;
3810
+ }
3811
+
3812
+ - (void)scheduleNativeToolbarSettlement {
3813
+ CGFloat width = CGRectGetWidth(_pagerScrollView.bounds);
3814
+ BOOL sameAttempt = _toolbarSettleAttempted && _toolbarSettleGeneration == _generation &&
3815
+ _toolbarSettleTransition == _transitionId && _toolbarSettlePage == _currentIndex &&
3816
+ _toolbarSettleWidth == width;
3817
+ if (sameAttempt) return;
3818
+ [self cancelNativeToolbarSettlement];
3819
+ _toolbarSettleAttempted = YES;
3820
+ _toolbarSettleGeneration = _generation;
3821
+ _toolbarSettleTransition = _transitionId;
3822
+ _toolbarSettlePage = _currentIndex;
3823
+ _toolbarSettleWidth = width;
3824
+ RNCPagerPresentationProgressProxy *proxy = [RNCPagerPresentationProgressProxy new];
3825
+ proxy.target = self;
3826
+ proxy.toolbarSettlement = YES;
3827
+ _toolbarSettleDisplayLink = [CADisplayLink displayLinkWithTarget:proxy selector:@selector(tick:)];
3828
+ [_toolbarSettleDisplayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
3829
+ }
3830
+
3831
+ - (void)sampleToolbarSettlement:(CADisplayLink *)link {
3832
+ if (link != _toolbarSettleDisplayLink) { [link invalidate]; return; }
3833
+ if (_toolbarSettleGeneration != _generation || _toolbarSettleTransition != _transitionId ||
3834
+ _toolbarSettlePage != _currentIndex ||
3835
+ _toolbarSettleWidth != CGRectGetWidth(_pagerScrollView.bounds) ||
3836
+ !self.window || _nativeTabBarView.window != self.window || ![_nativeTabBarView ownsSettledToolbar]) {
3837
+ [self cancelNativeToolbarSettlement];
3838
+ return;
3839
+ }
3840
+ ++_toolbarSettleTicks;
3841
+ [self syncNativeToolbarState];
3842
+ if (link == _toolbarSettleDisplayLink && _toolbarSettleTicks >= 2) {
3843
+ [link invalidate];
3844
+ _toolbarSettleDisplayLink = nil;
3845
+ }
3846
+ }
3847
+
3848
+ - (void)syncNativeToolbarState {
3849
+ BOOL logicalBusy = !self.window || _isBeingRecycled || !_hasAppliedInitialPage || _toolbarTransitionPending ||
3850
+ _transitioning || _isPagerDragging || _externalOuterTransitioning ||
3851
+ (_externalOuterPager != nil && !_externalOuterPageActive) || _needsSlotRebuild ||
3852
+ _pendingGoToIndex >= 0 || _transitionPageOffsets != nil || _pendingNativePagerScrollCompletion != nil ||
3853
+ _tabProgressDisplayLink != nil || _currentIndex < 0 || _currentIndex >= _pageControllers.count ||
3854
+ CGRectGetWidth(self.bounds) <= 0 || CGRectGetHeight(self.bounds) <= 0 ||
3855
+ _pagerScrollView.dragging || _pagerScrollView.decelerating;
3856
+ BOOL busy = logicalBusy || _pagerScrollView.tracking;
3857
+ if (!busy && _directNativePagerEnabled) {
3858
+ CALayer *presentation = _pagerScrollView.layer.presentationLayer;
3859
+ CGFloat offset = presentation ? presentation.bounds.origin.x : _pagerScrollView.contentOffset.x;
3860
+ busy = CGRectGetWidth(_pagerScrollView.bounds) <= 0 || !std::isfinite(offset) ||
3861
+ fabs(offset - [self directPagerOffsetForIndex:_currentIndex]) >= 0.5;
3862
+ } else if (!busy) {
3863
+ busy = _pageViewController.viewControllers.firstObject != _pageControllers[_currentIndex];
3864
+ }
3865
+ [_nativeTabBarView updateToolbarPagerIndex:_currentIndex busy:busy];
3866
+ if (busy && !logicalBusy && [_nativeTabBarView ownsSettledToolbar] &&
3867
+ _nativeTabBarView.window == self.window && !_nativeTabBarView.hidden &&
3868
+ CGRectGetWidth(_pagerScrollView.bounds) > 0) {
3869
+ [self scheduleNativeToolbarSettlement];
3870
+ } else {
3871
+ [self cancelNativeToolbarSettlement];
3872
+ }
3873
+ }
3874
+
3071
3875
  - (void)beginPageStateTransition
3072
3876
  {
3877
+ [self cancelNativeToolbarSettlement];
3878
+ _toolbarTransitionPending = YES;
3879
+ [_nativeTabBarView updateToolbarPagerIndex:-1 busy:YES];
3073
3880
  [self cancelPagerTop];
3074
3881
  if (!_nativeSmoothHeaderScrollEnabled || _headerHeight <= 0 || _transitionPageOffsets != nil) return;
3075
3882
  _transitionSourceDisplayBody = MAX(_observedScrollView.contentOffset.y + _observedScrollView.contentInset.top - _headerHeight, 0);
@@ -3444,7 +4251,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3444
4251
  animated:NO
3445
4252
  completion:nil];
3446
4253
  }
3447
- [_nativeTabBarView setProgress:_currentIndex];
4254
+ [self updateNativeTabProgress:_currentIndex];
3448
4255
  }
3449
4256
  if (_stickyHeaderView != nil) [_sharedHeaderHostView bringSubviewToFront:_stickyHeaderView];
3450
4257
  if (!_nativeTabBarView.hidden) [_sharedHeaderHostView bringSubviewToFront:_nativeTabBarView];
@@ -3462,6 +4269,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3462
4269
  - (void)layoutSubviews
3463
4270
  {
3464
4271
  [super layoutSubviews];
4272
+ if (_tabProgressDisplayLink != nil && fabs(CGRectGetWidth(self.bounds) - _tabProgressWidth) >= 0.5) {
4273
+ [self stopProgrammaticTabProgress];
4274
+ }
3465
4275
  void (^resizedPagerCompletion)(BOOL) = nil;
3466
4276
  NSInteger resizedPagerDestination = -1;
3467
4277
  NSUInteger resizedPagerGeneration = 0;
@@ -3504,7 +4314,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3504
4314
  animated:NO
3505
4315
  completion:nil];
3506
4316
  }
3507
- [_nativeTabBarView setProgress:_currentIndex];
4317
+ [self updateNativeTabProgress:_currentIndex];
3508
4318
  }
3509
4319
  // UIKit frame assignment is undefined while a view has a non-identity transform.
3510
4320
  // Restore transforms before settling page and shared-header frames.
@@ -3556,10 +4366,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3556
4366
  ) animated:NO];
3557
4367
  resizedPagerCompletion(YES);
3558
4368
  }
4369
+ [self syncNativeToolbarState];
3559
4370
  }
3560
4371
 
3561
4372
  - (void)prepareForRecycle
3562
4373
  {
4374
+ [self cancelNativeToolbarSettlement];
3563
4375
  [self cancelPendingNativePagerScroll];
3564
4376
  [_externalOuterPager removeNestedOverlayObserver:self];
3565
4377
  _externalOuterPager = nil;
@@ -3569,6 +4381,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3569
4381
  [self cancelHeaderSettledCheck];
3570
4382
  [self publishHeaderSettledAtTop:NO identityChanged:NO];
3571
4383
  _isBeingRecycled = YES;
4384
+ _toolbarTransitionPending = NO;
4385
+ [_nativeTabBarView updateToolbarPagerIndex:-1 busy:YES];
3572
4386
  _pendingTopOverlayAppearanceCommit = NO;
3573
4387
  [self syncScrollEdgeAppearance];
3574
4388
  _generation++;
@@ -3706,7 +4520,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3706
4520
  _needsPropsReapply = YES;
3707
4521
  _layoutDirection = @"ltr";
3708
4522
  [_nativeTabBarView updateItemsJSON:@"[]"];
3709
- [_nativeTabBarView setProgress:0];
4523
+ [_nativeTabBarView updateConfigJSON:@"{}"];
4524
+ [self updateNativeTabProgress:0];
3710
4525
  [_nativeSubHeaderView updateConfigJSON:@"{}"];
3711
4526
  _isBeingRecycled = NO;
3712
4527
  }
@@ -3835,6 +4650,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3835
4650
  _retainedPages = RCTNSStringFromString(newViewProps.retainedPages);
3836
4651
  [self setNeedsLayout];
3837
4652
  }
4653
+ if (_needsPropsReapply || oldViewProps.nativeTabBarConfig != newViewProps.nativeTabBarConfig) {
4654
+ [_nativeTabBarView updateConfigJSON:RCTNSStringFromString(newViewProps.nativeTabBarConfig)];
4655
+ }
3838
4656
  if (_needsPropsReapply || oldViewProps.nativeTabBarItems != newViewProps.nativeTabBarItems) {
3839
4657
  NSString *itemsJSON = RCTNSStringFromString(newViewProps.nativeTabBarItems);
3840
4658
  [_nativeTabBarView updateItemsJSON:itemsJSON];
@@ -3899,6 +4717,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3899
4717
  [self logHomeVisualGeometry:@"props-inset" scrollView:_observedScrollView];
3900
4718
  }
3901
4719
  _applyingTopOverlayInsetChange = NO;
4720
+ [self syncNativeToolbarState];
3902
4721
  if (!_nativeTabBarView.hidden) {
3903
4722
  [_nativeTabBarView.superview bringSubviewToFront:_nativeTabBarView];
3904
4723
  }
@@ -5324,6 +6143,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5324
6143
 
5325
6144
  - (void)cancelPendingNativePagerScroll
5326
6145
  {
6146
+ [self stopProgrammaticTabProgress];
5327
6147
  if (_pendingNativePagerScrollCompletion == nil) return;
5328
6148
  _pendingNativePagerScrollCompletion = nil;
5329
6149
  _pendingNativePagerScrollGeneration = 0;
@@ -5342,6 +6162,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5342
6162
  {
5343
6163
  [self cancelTopInsetReboundCheck];
5344
6164
  if (_isBeingRecycled || index < 0 || index >= _pageControllers.count) return;
6165
+ [self cancelNativeToolbarSettlement];
5345
6166
  _hasReceivedPageCommand = YES;
5346
6167
  _hasAppliedInitialPage = YES;
5347
6168
  if (_transitioning || _isPagerDragging) {
@@ -5363,6 +6184,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5363
6184
  [self attachScrollObserverForCurrentPage];
5364
6185
  [self emitPageSelected:index];
5365
6186
  [self emitDiagnostics:@"page-selected"];
6187
+ [self syncNativeToolbarState];
5366
6188
  return;
5367
6189
  }
5368
6190
 
@@ -5391,13 +6213,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5391
6213
  CGFloat targetOffset = [self directPagerOffsetForIndex:index];
5392
6214
  void (^animations)(void) = ^{
5393
6215
  [self->_pagerScrollView setContentOffset:CGPointMake(targetOffset, 0)];
5394
- [self->_nativeTabBarView setProgress:index];
6216
+ if (self->_tabProgressDisplayLink == nil) [self->_nativeTabBarView setProgress:index];
5395
6217
  [self updatePageStateTransitionProgress:index];
5396
6218
  };
5397
6219
  void (^completion)(BOOL) = ^(BOOL finished) {
5398
6220
  __strong __typeof__(weakSelf) self = weakSelf;
5399
6221
  if (self == nil || self->_generation != capturedGeneration ||
5400
6222
  self->_transitionId != capturedTransition || self->_isBeingRecycled) return;
6223
+ [self stopProgrammaticTabProgress];
5401
6224
  self->_pagerScrollView.scrollEnabled = self->_scrollEnabled;
5402
6225
  if (finished) {
5403
6226
  [self detachScrollObserver];
@@ -5433,7 +6256,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5433
6256
  _pagerScrollView.scrollEnabled = NO;
5434
6257
  if (useNativeAnimatedScroll) {
5435
6258
  if (fabs(_pagerScrollView.contentOffset.x - targetOffset) <= 0.5) {
5436
- [_nativeTabBarView setProgress:index];
6259
+ [self updateNativeTabProgress:index];
5437
6260
  [self updatePageStateTransitionProgress:index];
5438
6261
  completion(YES);
5439
6262
  } else {
@@ -5444,6 +6267,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5444
6267
  [_pagerScrollView setContentOffset:CGPointMake(targetOffset, 0) animated:YES];
5445
6268
  }
5446
6269
  } else {
6270
+ if ([_nativeTabBarView usesControlledTextProgress]) [self startProgrammaticTabProgress];
5447
6271
  [UIView animateWithDuration:0.28
5448
6272
  delay:0
5449
6273
  options:UIViewAnimationOptionCurveEaseInOut |
@@ -5508,10 +6332,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5508
6332
  [self attachScrollObserverForCurrentPage];
5509
6333
  }
5510
6334
  [self drainPendingGoTo];
6335
+ if (!self->_transitioning && !self->_isPagerDragging && self->_pendingGoToIndex < 0) self->_toolbarTransitionPending = NO;
5511
6336
  if (!hadProgrammaticScrollEdges && !self->_transitioning && !self->_needsSlotRebuild) {
5512
6337
  [self attachScrollObserverForCurrentPage];
5513
6338
  }
5514
6339
  [self updateScrollEdgeContentMasks];
6340
+ [self syncNativeToolbarState];
5515
6341
  });
5516
6342
  }
5517
6343
 
@@ -5631,7 +6457,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5631
6457
  [self directPagerOffsetForIndex:index],
5632
6458
  0
5633
6459
  ) animated:NO];
5634
- [_nativeTabBarView setProgress:index];
6460
+ [self updateNativeTabProgress:index];
5635
6461
  [self attachScrollObserverForCurrentPageAfterTransition:YES];
5636
6462
  if (emitSelection &&
5637
6463
  (_pendingGoToIndex < 0 || _pendingGoToIndex == index)) {
@@ -5682,7 +6508,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5682
6508
  [self detachScrollObserver];
5683
6509
  _currentIndex = index;
5684
6510
  _destinationIndex = index;
5685
- [_nativeTabBarView setProgress:index];
6511
+ [self updateNativeTabProgress:index];
5686
6512
  [self attachScrollObserverForCurrentPageAfterTransition:YES];
5687
6513
  // A newer tab tap remains authoritative while its queued command runs.
5688
6514
  if (emitSelection &&
@@ -5743,7 +6569,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5743
6569
  position = MAX(0, MIN(position, (NSInteger)_pageControllers.count - 1));
5744
6570
  [self updatePageStateTransitionProgress:progress];
5745
6571
  [self updateScrollEdgeContentMasks];
5746
- [_nativeTabBarView setProgress:progress];
6572
+ if (_tabProgressDisplayLink == nil) [self updateNativeTabProgress:progress];
5747
6573
  const auto emitter = [self eventEmitter];
5748
6574
  if (emitter) {
5749
6575
  emitter->onPageScroll({.position = (double)position, .offset = (double)offset});
@@ -5762,7 +6588,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
5762
6588
  }
5763
6589
  [self updatePageStateTransitionProgress:tabProgress];
5764
6590
  [self updateScrollEdgeContentMasks];
5765
- [_nativeTabBarView setProgress:tabProgress];
6591
+ [self updateNativeTabProgress:tabProgress];
5766
6592
  const auto emitter = [self eventEmitter];
5767
6593
  if (emitter) {
5768
6594
  emitter->onPageScroll({.position = (double)position, .offset = (double)offset});