react-native-navigation 8.8.2 → 8.8.3-snapshot.2506

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 (74) hide show
  1. package/android/src/main/java/com/reactnativenavigation/hierarchy/root/RootAnimator.kt +5 -2
  2. package/android/src/main/java/com/reactnativenavigation/react/DevBundleDownloadListenerAdapter.java +5 -8
  3. package/android/src/main/java/com/reactnativenavigation/react/JsDevReloadHandlerFacade.java +5 -8
  4. package/android/src/main/java/com/reactnativenavigation/react/NavigationTurboModule.kt +5 -0
  5. package/android/src/main/java/com/reactnativenavigation/react/ReloadHandlerFacade.java +5 -8
  6. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerBuilder.java +2 -1
  7. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/viewcontroller/LayoutDirectionApplier.kt +4 -1
  8. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/viewcontroller/Presenter.java +3 -1
  9. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/viewcontroller/RootPresenter.java +11 -1
  10. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/viewcontroller/ViewController.java +1 -1
  11. package/android/src/test/java/com/reactnativenavigation/BaseRobolectricTest.kt +1 -1
  12. package/android/src/test/java/com/reactnativenavigation/BaseTest.kt +3 -2
  13. package/android/src/test/java/com/reactnativenavigation/ShadowReactView.java +45 -0
  14. package/android/src/test/java/com/reactnativenavigation/ShadowSoLoader.java +28 -0
  15. package/android/src/test/java/com/reactnativenavigation/TestApplication.kt +2 -2
  16. package/android/src/test/java/com/reactnativenavigation/mocks/SimpleViewController.java +14 -7
  17. package/android/src/test/java/com/reactnativenavigation/presentation/PresenterTest.java +0 -1
  18. package/android/src/test/java/com/reactnativenavigation/utils/ButtonPresenterTest.java +0 -1
  19. package/android/src/test/java/com/reactnativenavigation/utils/LayoutFactoryTest.java +0 -1
  20. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java +0 -1
  21. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenterTest.java +0 -1
  22. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.kt +0 -1
  23. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsPresenterTest.kt +0 -1
  24. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/component/ComponentViewControllerTest.java +0 -1
  25. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/externalcomponent/ExternalComponentViewControllerTest.java +0 -1
  26. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalAnimatorTest.kt +0 -1
  27. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java +0 -1
  28. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalStackTest.java +0 -1
  29. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java +0 -1
  30. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.kt +0 -1
  31. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/overlay/OverlayManagerTest.java +0 -1
  32. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/parent/ParentControllerTest.java +0 -1
  33. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/sidemenu/SideMenuControllerTest.java +0 -1
  34. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/stack/BackButtonHelperTest.java +0 -1
  35. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/stack/FloatingActionButtonTest.java +0 -1
  36. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackAnimatorTest.kt +0 -1
  37. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.kt +0 -1
  38. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackPresenterTest.kt +0 -1
  39. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/stack/TitleBarButtonControllerTest.java +0 -1
  40. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/stack/TitleBarReactViewControllerTest.java +0 -1
  41. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/stack/TopBarButtonControllerTest.java +0 -1
  42. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/stack/TopBarControllerTest.kt +0 -1
  43. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsViewControllerTest.java +0 -1
  44. package/android/src/test/java/com/reactnativenavigation/viewcontrollers/viewcontroller/ViewControllerTest.java +0 -1
  45. package/android/src/test/java/com/reactnativenavigation/views/TitleAndButtonsContainerTest.kt +0 -1
  46. package/android/src/test/java/com/reactnativenavigation/views/TitleSubTitleLayoutTest.kt +0 -1
  47. package/ios/BottomTabPresenter.h +2 -0
  48. package/ios/BottomTabPresenter.mm +1 -3
  49. package/ios/BottomTabsAfterInitialTabAttacher.mm +4 -0
  50. package/ios/BottomTabsTogetherAttacher.mm +4 -0
  51. package/ios/RNNAppDelegate.mm +15 -4
  52. package/ios/RNNBackButtonOptions.h +1 -0
  53. package/ios/RNNBackButtonOptions.mm +3 -0
  54. package/ios/RNNBottomTabOptions.h +1 -0
  55. package/ios/RNNBottomTabOptions.mm +4 -1
  56. package/ios/RNNBottomTabsController.mm +48 -8
  57. package/ios/RNNButtonOptions.h +1 -0
  58. package/ios/RNNButtonOptions.mm +4 -0
  59. package/ios/RNNComponentOptions.mm +1 -1
  60. package/ios/RNNComponentViewController.mm +5 -0
  61. package/ios/RNNScreenTransition.mm +1 -1
  62. package/ios/RNNStackController.mm +76 -1
  63. package/ios/RNNTabBarItemCreator.h +2 -0
  64. package/ios/RNNTabBarItemCreator.mm +98 -0
  65. package/ios/RNNUIBarButtonItem.mm +26 -0
  66. package/ios/StackControllerDelegate.mm +1 -0
  67. package/ios/TopBarPresenter.mm +26 -2
  68. package/ios/TransitionOptions.mm +2 -2
  69. package/ios/UIViewController+LayoutProtocol.h +2 -0
  70. package/lib/module/interfaces/Options.js.map +1 -1
  71. package/lib/typescript/interfaces/Options.d.ts +42 -0
  72. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  73. package/package.json +13 -12
  74. package/src/interfaces/Options.ts +54 -0
@@ -40,7 +40,6 @@ import static org.mockito.Mockito.spy;
40
40
  import static org.mockito.Mockito.times;
41
41
  import static org.mockito.Mockito.verify;
42
42
 
43
- @Ignore("New architecture - WIP")
44
43
  public class TopTabsViewControllerTest extends BaseTest {
45
44
  private static final int SIZE = 2;
46
45
 
@@ -40,7 +40,6 @@ import static org.mockito.Mockito.times;
40
40
  import static org.mockito.Mockito.verify;
41
41
  import static org.mockito.Mockito.withSettings;
42
42
 
43
- @Ignore("New architecture - WIP")
44
43
  public class ViewControllerTest extends BaseTest {
45
44
 
46
45
  private ViewController uut;
@@ -30,7 +30,6 @@ import kotlin.test.assertFalse
30
30
  private const val UUT_WIDTH = 1000
31
31
  private const val UUT_HEIGHT = 100
32
32
 
33
- @Ignore("New architecture - failed to fix")
34
33
  class TitleAndButtonsContainerTest : BaseTest() {
35
34
  lateinit var uut: TitleAndButtonsContainer
36
35
  private lateinit var activity: Activity
@@ -12,7 +12,6 @@ import org.junit.Test
12
12
  private const val UUT_WIDTH = 1000
13
13
  private const val UUT_HEIGHT = 100
14
14
 
15
- @Ignore("New architecture - WIP")
16
15
  class TitleSubTitleLayoutTest : BaseTest() {
17
16
  private val testId = "mock-testId"
18
17
 
@@ -2,6 +2,8 @@
2
2
  #import "RNNTabBarItemCreator.h"
3
3
  @interface BottomTabPresenter : RNNBasePresenter
4
4
 
5
+ @property(nonatomic, strong, readonly) RNNTabBarItemCreator *tabCreator;
6
+
5
7
  - (instancetype)initWithDefaultOptions:(RNNNavigationOptions *)defaultOptions
6
8
  tabCreator:(RNNTabBarItemCreator *)tabCreator;
7
9
 
@@ -3,9 +3,7 @@
3
3
  #import "UIViewController+LayoutProtocol.h"
4
4
  #import "UIViewController+RNNOptions.h"
5
5
 
6
- @implementation BottomTabPresenter {
7
- RNNTabBarItemCreator *_tabCreator;
8
- }
6
+ @implementation BottomTabPresenter
9
7
 
10
8
  - (instancetype)initWithDefaultOptions:(RNNNavigationOptions *)defaultOptions
11
9
  tabCreator:(RNNTabBarItemCreator *)tabCreator {
@@ -9,6 +9,10 @@
9
9
  for (UIViewController *viewController in bottomTabsController.deselectedViewControllers) {
10
10
  dispatch_async(dispatch_get_main_queue(), ^{
11
11
  UIWindow *preloadWindow = [[UIWindow alloc] initWithFrame:CGRectZero];
12
+ // Clip the preload window so the deselected tab's full-screen reactView,
13
+ // which is briefly hosted here while it renders, can't draw outside the
14
+ // zero-frame window and flicker over the already-visible selected tab.
15
+ preloadWindow.clipsToBounds = YES;
12
16
  preloadWindow.hidden = NO;
13
17
 
14
18
  dispatch_group_t ready = dispatch_group_create();
@@ -7,6 +7,10 @@
7
7
  dispatch_group_t ready = dispatch_group_create();
8
8
 
9
9
  UIWindow *preloadWindow = [[UIWindow alloc] initWithFrame:CGRectZero];
10
+ // Clip the preload window so the full-screen reactViews hosted here while
11
+ // they render can't draw outside the zero-frame window. Today the splash
12
+ // screen masks this, but without clipping it's a latent flicker.
13
+ preloadWindow.clipsToBounds = YES;
10
14
  preloadWindow.hidden = NO;
11
15
 
12
16
  NSMapTable *reactViewToParent = [NSMapTable strongToStrongObjectsMapTable];
@@ -4,7 +4,6 @@
4
4
  #import <react/featureflags/ReactNativeFeatureFlagsDefaults.h>
5
5
 
6
6
  #import "RCTAppSetupUtils.h"
7
- #import <React/CoreModulesPlugins.h>
8
7
  #if __has_include(<React/RCTCxxBridgeDelegate.h>)
9
8
  #import <React/RCTCxxBridgeDelegate.h>
10
9
  #endif
@@ -56,12 +55,17 @@ static NSString *const kRNConcurrentRoot = @"concurrentRoot";
56
55
  - (BOOL)application:(UIApplication *)application
57
56
  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
58
57
 
58
+ // RN 0.77 & RN0.78
59
59
  #if !RNN_RN_VERSION_79_OR_NEWER
60
- [self _setUpFeatureFlags];
60
+ #if __has_include(<ReactAppDependencyProvider/RCTAppDependencyProvider.h>)
61
+ self.dependencyProvider = [RCTAppDependencyProvider new];
62
+ #endif
63
+ // RN 0.77
61
64
  #if !RNN_RN_VERSION_78
65
+ [self _setUpFeatureFlags];
62
66
  self.rootViewFactory = [self createRCTRootViewFactory];
63
- #else
64
- self.reactNativeFactory = [[RCTReactNativeFactory alloc] init];
67
+ #else // RN0.78
68
+ self.reactNativeFactory = [[RCTReactNativeFactory alloc] initWithDelegate:self];
65
69
  self.reactNativeFactory.rootViewFactory = [self createRCTRootViewFactory];
66
70
  #endif
67
71
 
@@ -119,6 +123,13 @@ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
119
123
  return [[RCTRootViewFactory alloc] initWithConfiguration:configuration andTurboModuleManagerDelegate:self];
120
124
  }
121
125
 
126
+ #pragma mark - RCTTurboModuleManagerDelegate
127
+
128
+ - (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
129
+ {
130
+ return RCTAppSetupDefaultModuleFromClass(moduleClass, self.dependencyProvider);
131
+ }
132
+
122
133
  #pragma mark - Feature Flags
123
134
  class RCTAppDelegateBridgelessFeatureFlags : public facebook::react::ReactNativeFeatureFlagsDefaults {
124
135
  public:
@@ -18,6 +18,7 @@
18
18
  @property(nonatomic, strong) Text *identifier;
19
19
  @property(nonatomic, strong) Bool *popStackOnPress;
20
20
  @property(nonatomic, strong) RNNIconBackgroundOptions *iconBackground;
21
+ @property(nonatomic, strong) Bool *hideSharedBackground;
21
22
 
22
23
  - (BOOL)hasValue;
23
24
 
@@ -20,6 +20,7 @@
20
20
  self.displayMode = [TextParser parse:dict key:@"displayMode"];
21
21
  self.popStackOnPress = [BoolParser parse:dict key:@"popStackOnPress"];
22
22
  self.iconBackground = [[RNNIconBackgroundOptions alloc] initWithDict:dict[@"iconBackground"] enabled:nil];
23
+ self.hideSharedBackground = [BoolParser parse:dict key:@"hideSharedBackground"];
23
24
 
24
25
  return self;
25
26
  }
@@ -54,6 +55,8 @@
54
55
  self.popStackOnPress = options.popStackOnPress;
55
56
  if (options.sfSymbol.hasValue)
56
57
  self.sfSymbol = options.sfSymbol;
58
+ if (options.hideSharedBackground.hasValue)
59
+ self.hideSharedBackground = options.hideSharedBackground;
57
60
  }
58
61
 
59
62
  - (BOOL)hasValue {
@@ -25,6 +25,7 @@
25
25
  @property(nonatomic, strong) Bool *selectTabOnPress;
26
26
  @property(nonatomic, strong) Text *sfSymbol;
27
27
  @property(nonatomic, strong) Text *sfSelectedSymbol;
28
+ @property(nonatomic, strong) Text *role;
28
29
 
29
30
  - (BOOL)hasValue;
30
31
 
@@ -31,6 +31,7 @@
31
31
  self.selectTabOnPress = [BoolParser parse:dict key:@"selectTabOnPress"];
32
32
  self.sfSymbol = [TextParser parse:dict key:@"sfSymbol"];
33
33
  self.sfSelectedSymbol = [TextParser parse:dict key:@"sfSelectedSymbol"];
34
+ self.role = [TextParser parse:dict key:@"role"];
34
35
 
35
36
  return self;
36
37
  }
@@ -76,6 +77,8 @@
76
77
  self.sfSymbol = options.sfSymbol;
77
78
  if (options.sfSelectedSymbol.hasValue)
78
79
  self.sfSelectedSymbol = options.sfSelectedSymbol;
80
+ if (options.role.hasValue)
81
+ self.role = options.role;
79
82
  }
80
83
 
81
84
  - (BOOL)hasValue {
@@ -85,7 +88,7 @@
85
88
  self.iconColor.hasValue || self.selectedIconColor.hasValue ||
86
89
  self.selectedTextColor.hasValue || self.iconInsets.hasValue || self.textColor.hasValue ||
87
90
  self.visible.hasValue || self.selectTabOnPress.hasValue || self.sfSymbol.hasValue ||
88
- self.sfSelectedSymbol.hasValue;
91
+ self.sfSelectedSymbol.hasValue || self.role.hasValue;
89
92
  }
90
93
 
91
94
  @end
@@ -1,4 +1,5 @@
1
1
  #import "RNNBottomTabsController.h"
2
+ #import "RNNTabBarItemCreator.h"
2
3
  #import "UITabBarController+RNNOptions.h"
3
4
  #import "UITabBarController+RNNUtils.h"
4
5
 
@@ -7,6 +8,8 @@
7
8
  @property(nonatomic, strong) RNNDotIndicatorPresenter *dotIndicatorPresenter;
8
9
  @property(nonatomic, strong) UILongPressGestureRecognizer *longPressRecognizer;
9
10
 
11
+ - (void)rnn_cycleAllTabsThenRestoreInitialSelection;
12
+
10
13
  @end
11
14
 
12
15
  @implementation RNNBottomTabsController {
@@ -16,6 +19,8 @@
16
19
  BOOL _tabBarNeedsRestore;
17
20
  RNNNavigationOptions *_options;
18
21
  BOOL _didFinishSetup;
22
+ BOOL _rnnDidApplyInitialTabBarSelectionFix;
23
+ BOOL _rnnSuppressTabSelectionEvents;
19
24
  }
20
25
 
21
26
  - (instancetype)initWithLayoutInfo:(RNNLayoutInfo *)layoutInfo
@@ -49,12 +54,7 @@
49
54
  eventEmitter:eventEmitter
50
55
  childViewControllers:childViewControllers];
51
56
 
52
- if (@available(iOS 26.0, *)) {
53
- UITabBarAppearance *appearance = [UITabBarAppearance new];
54
- [appearance configureWithDefaultBackground];
55
- self.tabBar.standardAppearance = appearance;
56
- self.tabBar.scrollEdgeAppearance = [appearance copy];
57
- } else if (@available(iOS 13.0, *)) {
57
+ if (@available(iOS 13.0, *)) {
58
58
  UITabBarAppearance *appearance = [UITabBarAppearance new];
59
59
  [appearance configureWithOpaqueBackground];
60
60
  appearance.backgroundEffect = nil;
@@ -90,11 +90,44 @@
90
90
  }
91
91
  }
92
92
 
93
- - (void)viewDidLoad {
94
- [super viewDidLoad];
93
+ - (void)viewDidAppear:(BOOL)animated {
94
+ [super viewDidAppear:animated];
95
+ // iOS 26: first layout can misplace tab item titles; cycling selection (then restoring) forces a
96
+ // correct layout without user interaction. Defer so all tab children are in the hierarchy.
97
+ if (@available(iOS 26.0, *)) {
98
+ if (!_rnnDidApplyInitialTabBarSelectionFix) {
99
+ _rnnDidApplyInitialTabBarSelectionFix = YES;
100
+ __weak RNNBottomTabsController *weakSelf = self;
101
+ dispatch_async(dispatch_get_main_queue(), ^{
102
+ [weakSelf rnn_cycleAllTabsThenRestoreInitialSelection];
103
+ });
104
+ }
105
+ }
106
+ }
107
+
108
+ - (void)rnn_cycleAllTabsThenRestoreInitialSelection {
109
+ NSUInteger count = self.childViewControllers.count;
110
+ if (count <= 1) {
111
+ return;
112
+ }
113
+
114
+ NSUInteger initial = _currentTabIndex;
115
+ if (initial >= count) {
116
+ initial = 0;
117
+ }
118
+
119
+ _rnnSuppressTabSelectionEvents = YES;
120
+ [UIView performWithoutAnimation:^{
121
+ for (NSUInteger i = 0; i < count; i++) {
122
+ [self setSelectedIndex:i];
123
+ }
124
+ [self setSelectedIndex:initial];
125
+ }];
126
+ _rnnSuppressTabSelectionEvents = NO;
95
127
  }
96
128
 
97
129
  - (void)createTabBarItems:(NSArray<UIViewController *> *)childViewControllers {
130
+ _bottomTabPresenter.tabCreator.searchRoleUsed = NO;
98
131
  for (UIViewController *child in childViewControllers) {
99
132
  [_bottomTabPresenter applyOptions:child.resolveOptions child:child];
100
133
  }
@@ -200,6 +233,9 @@
200
233
 
201
234
  - (void)tabBarController:(UITabBarController *)tabBarController
202
235
  didSelectViewController:(UIViewController *)viewController {
236
+ if (_rnnSuppressTabSelectionEvents) {
237
+ return;
238
+ }
203
239
  [self.eventEmitter sendBottomTabSelected:@(tabBarController.selectedIndex)
204
240
  unselected:@(_previousTabIndex)];
205
241
  }
@@ -216,6 +252,10 @@
216
252
  NSUInteger _index = [tabBarController.viewControllers indexOfObject:viewController];
217
253
  BOOL isMoreTab = ![tabBarController.viewControllers containsObject:viewController];
218
254
 
255
+ if (_rnnSuppressTabSelectionEvents) {
256
+ return YES;
257
+ }
258
+
219
259
  [self.eventEmitter sendBottomTabPressed:@(_index)];
220
260
 
221
261
  if ([[viewController resolveOptions].bottomTab.selectTabOnPress withDefault:YES] || isMoreTab) {
@@ -23,6 +23,7 @@
23
23
  @property(nonatomic, strong) RNNComponentOptions *component;
24
24
  @property(nonatomic, strong) RNNIconBackgroundOptions *iconBackground;
25
25
  @property(nonatomic, strong) Bool *disableIconTint;
26
+ @property(nonatomic, strong) Bool *hideSharedBackground;
26
27
 
27
28
  - (RNNButtonOptions *)withDefault:(RNNButtonOptions *)defaultOptions;
28
29
 
@@ -25,6 +25,7 @@
25
25
  enabled:self.enabled];
26
26
  self.systemItem = [TextParser parse:dict key:@"systemItem"];
27
27
  self.disableIconTint = [BoolParser parse:dict key:@"disableIconTint"];
28
+ self.hideSharedBackground = [BoolParser parse:dict key:@"hideSharedBackground"];
28
29
 
29
30
  return self;
30
31
  }
@@ -49,6 +50,7 @@
49
50
  newOptions.iconBackground = self.iconBackground.copy;
50
51
  newOptions.systemItem = self.systemItem.copy;
51
52
  newOptions.disableIconTint = self.disableIconTint.copy;
53
+ newOptions.hideSharedBackground = self.hideSharedBackground.copy;
52
54
  return newOptions;
53
55
  }
54
56
 
@@ -88,6 +90,8 @@
88
90
  self.systemItem = options.systemItem;
89
91
  if (options.disableIconTint.hasValue)
90
92
  self.disableIconTint = options.disableIconTint;
93
+ if (options.hideSharedBackground.hasValue)
94
+ self.hideSharedBackground = options.hideSharedBackground;
91
95
  }
92
96
 
93
97
  - (BOOL)shouldCreateCustomView {
@@ -9,7 +9,7 @@
9
9
  self.name = [TextParser parse:dict key:@"name"];
10
10
  self.componentId = [TextParser parse:dict key:@"componentId"];
11
11
  self.alignment = [TextParser parse:dict key:@"alignment"];
12
- self.waitForRender = [Bool withValue:[[BoolParser parse:dict key:@"waitForRender"] withDefault:[RNNUtils getDefaultWaitForRender]]];
12
+ self.waitForRender = [BoolParser parse:dict key:@"waitForRender"];
13
13
 
14
14
  return self;
15
15
  }
@@ -155,6 +155,11 @@
155
155
  isFocused:searchController.searchBar.isFirstResponder];
156
156
  }
157
157
 
158
+ - (void)destroyReactView {
159
+ [self.reactView removeFromSuperview];
160
+ self.reactView = nil;
161
+ }
162
+
158
163
  - (void)screenPopped {
159
164
  [_eventEmitter sendScreenPoppedEvent:self.layoutInfo.componentId];
160
165
  }
@@ -11,7 +11,7 @@
11
11
  self.content = [[RNNEnterExitAnimation alloc] initWithDict:dict[@"content"]];
12
12
  self.bottomTabs = [[ElementTransitionOptions alloc] initWithDict:dict[@"bottomTabs"]];
13
13
  self.enable = [BoolParser parse:dict key:@"enabled"];
14
- self.waitForRender = [Bool withValue: [[BoolParser parse:dict key:@"waitForRender"] withDefault: [RNNUtils getDefaultWaitForRender]]];
14
+ self.waitForRender = [BoolParser parse:dict key:@"waitForRender"];
15
15
  self.duration = [TimeIntervalParser parse:dict key:@"duration"];
16
16
  self.sharedElementTransitions = [OptionsArrayParser parse:dict
17
17
  key:@"sharedElementTransitions"
@@ -42,7 +42,82 @@
42
42
 
43
43
  - (UIViewController *)popViewControllerAnimated:(BOOL)animated {
44
44
  [self prepareForPop];
45
- return [super popViewControllerAnimated:animated];
45
+ UIViewController *previousTop = self.topViewController;
46
+ UIView *snapshot = [self snapshotTopView:animated];
47
+
48
+ UIViewController *poppedVC = [super popViewControllerAnimated:animated];
49
+ if (!poppedVC) {
50
+ [snapshot removeFromSuperview];
51
+ return nil;
52
+ }
53
+
54
+ id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
55
+ if (coordinator && coordinator.isInteractive) {
56
+ // Interactive pop (swipe-back): remove snapshot overlay — UIKit shows the live
57
+ // view during the gesture. Skip early teardown so the React view stays alive
58
+ // if the gesture is cancelled. The delegate's didShowViewController handles
59
+ // cleanup once the animation finishes.
60
+ [snapshot removeFromSuperview];
61
+ } else {
62
+ [self teardownPoppedControllers:@[ poppedVC ] previousTop:previousTop];
63
+ }
64
+ return poppedVC;
65
+ }
66
+
67
+ - (NSArray<UIViewController *> *)popToViewController:(UIViewController *)viewController
68
+ animated:(BOOL)animated {
69
+ UIViewController *previousTop = self.topViewController;
70
+ UIView *snapshot = [self snapshotTopView:animated];
71
+
72
+ NSArray<UIViewController *> *poppedVCs =
73
+ [super popToViewController:viewController animated:animated];
74
+ if (poppedVCs.count > 0) {
75
+ [self teardownPoppedControllers:poppedVCs previousTop:previousTop];
76
+ } else {
77
+ [snapshot removeFromSuperview];
78
+ }
79
+ return poppedVCs;
80
+ }
81
+
82
+ - (NSArray<UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated {
83
+ UIViewController *previousTop = self.topViewController;
84
+ UIView *snapshot = [self snapshotTopView:animated];
85
+
86
+ NSArray<UIViewController *> *poppedVCs =
87
+ [super popToRootViewControllerAnimated:animated];
88
+ if (poppedVCs.count > 0) {
89
+ [self teardownPoppedControllers:poppedVCs previousTop:previousTop];
90
+ } else {
91
+ [snapshot removeFromSuperview];
92
+ }
93
+ return poppedVCs;
94
+ }
95
+
96
+ #pragma mark - React view teardown
97
+
98
+ - (UIView *)snapshotTopView:(BOOL)animated {
99
+ if (!animated) return nil;
100
+ UIViewController *topVC = self.topViewController;
101
+ if (!topVC.isViewLoaded || !topVC.view.window) return nil;
102
+
103
+ UIView *snapshot = [topVC.view snapshotViewAfterScreenUpdates:NO];
104
+ if (snapshot) {
105
+ snapshot.frame = topVC.view.bounds;
106
+ snapshot.autoresizingMask =
107
+ UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
108
+ [topVC.view addSubview:snapshot];
109
+ }
110
+ return snapshot;
111
+ }
112
+
113
+ - (void)teardownPoppedControllers:(NSArray<UIViewController *> *)poppedVCs
114
+ previousTop:(UIViewController *)previousTop {
115
+ for (UIViewController *vc in poppedVCs) {
116
+ if (vc == previousTop && [vc isKindOfClass:[RNNComponentViewController class]]) {
117
+ [[(RNNComponentViewController *)vc reactView] componentDidDisappear];
118
+ }
119
+ [vc destroyReactView];
120
+ }
46
121
  }
47
122
 
48
123
  - (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
@@ -4,6 +4,8 @@
4
4
 
5
5
  @interface RNNTabBarItemCreator : NSObject
6
6
 
7
+ @property(nonatomic, assign) BOOL searchRoleUsed;
8
+
7
9
  - (UITabBarItem *)createTabBarItem:(RNNBottomTabOptions *)bottomTabOptions
8
10
  mergeItem:(UITabBarItem *)mergeItem;
9
11
 
@@ -1,15 +1,46 @@
1
1
  #import "RNNTabBarItemCreator.h"
2
2
  #import "RNNFontAttributesCreator.h"
3
3
  #import "UIImage+utils.h"
4
+ #import <React/RCTLog.h>
4
5
 
5
6
  @implementation RNNTabBarItemCreator
6
7
 
8
+ + (NSNumber *)systemItemForRole:(NSString *)role {
9
+ static NSDictionary<NSString *, NSNumber *> *map = nil;
10
+ static dispatch_once_t onceToken;
11
+ dispatch_once(&onceToken, ^{
12
+ map = @{
13
+ @"search" : @(UITabBarSystemItemSearch),
14
+ @"bookmarks" : @(UITabBarSystemItemBookmarks),
15
+ @"contacts" : @(UITabBarSystemItemContacts),
16
+ @"downloads" : @(UITabBarSystemItemDownloads),
17
+ @"favorites" : @(UITabBarSystemItemFavorites),
18
+ @"featured" : @(UITabBarSystemItemFeatured),
19
+ @"history" : @(UITabBarSystemItemHistory),
20
+ @"more" : @(UITabBarSystemItemMore),
21
+ @"mostRecent" : @(UITabBarSystemItemMostRecent),
22
+ @"mostViewed" : @(UITabBarSystemItemMostViewed),
23
+ @"recents" : @(UITabBarSystemItemRecents),
24
+ @"topRated" : @(UITabBarSystemItemTopRated),
25
+ };
26
+ });
27
+ return map[role];
28
+ }
29
+
7
30
  - (UITabBarItem *)createTabBarItem:(UITabBarItem *)mergeItem {
8
31
  return mergeItem ?: [UITabBarItem new];
9
32
  }
10
33
 
11
34
  - (UITabBarItem *)createTabBarItem:(RNNBottomTabOptions *)bottomTabOptions
12
35
  mergeItem:(UITabBarItem *)mergeItem {
36
+
37
+ if (bottomTabOptions.role.hasValue) {
38
+ UITabBarItem *roleItem = [self createSystemItemForRole:bottomTabOptions
39
+ mergeItem:mergeItem];
40
+ if (roleItem)
41
+ return roleItem;
42
+ }
43
+
13
44
  UITabBarItem *tabItem = [self createTabBarItem:mergeItem];
14
45
  UIImage *icon = [bottomTabOptions.icon withDefault:nil];
15
46
  UIImage *selectedIcon = [bottomTabOptions.selectedIcon withDefault:icon];
@@ -55,6 +86,71 @@
55
86
  return tabItem;
56
87
  }
57
88
 
89
+ #pragma mark - Role (system item) creation
90
+
91
+ - (UITabBarItem *)createSystemItemForRole:(RNNBottomTabOptions *)bottomTabOptions
92
+ mergeItem:(UITabBarItem *)mergeItem {
93
+ NSString *role = [bottomTabOptions.role withDefault:nil];
94
+ NSNumber *systemItemNumber = [RNNTabBarItemCreator systemItemForRole:role];
95
+
96
+ if (!systemItemNumber) {
97
+ RCTLogWarn(@"[RNN] Unknown bottomTab role '%@' — falling back to normal tab.", role);
98
+ return nil;
99
+ }
100
+
101
+ if ([role isEqualToString:@"search"] && self.searchRoleUsed) {
102
+ RCTLogWarn(@"[RNN] Only one tab per bottomTabs layout may use role:'search'. "
103
+ @"Subsequent 'search' roles are ignored — creating a normal tab instead.");
104
+ return nil;
105
+ }
106
+
107
+ if ([role isEqualToString:@"search"]) {
108
+ self.searchRoleUsed = YES;
109
+ }
110
+
111
+ UITabBarItem *tabItem =
112
+ [[UITabBarItem alloc] initWithTabBarSystemItem:(UITabBarSystemItem)[systemItemNumber integerValue]
113
+ tag:bottomTabOptions.tag];
114
+
115
+ UITabBarItem *baseItem = [self createTabBarItem:mergeItem];
116
+ tabItem.standardAppearance = baseItem.standardAppearance;
117
+ #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
118
+ if (@available(iOS 15.0, *)) {
119
+ tabItem.scrollEdgeAppearance = baseItem.scrollEdgeAppearance;
120
+ }
121
+ #endif
122
+
123
+ UIImage *icon = [bottomTabOptions.icon withDefault:nil];
124
+ UIImage *selectedIcon = [bottomTabOptions.selectedIcon withDefault:nil];
125
+ UIColor *iconColor = [bottomTabOptions.iconColor withDefault:nil];
126
+ UIColor *selectedIconColor = [bottomTabOptions.selectedIconColor withDefault:iconColor];
127
+
128
+ if (@available(iOS 13.0, *)) {
129
+ if (bottomTabOptions.sfSymbol.hasValue) {
130
+ icon = [UIImage systemImageNamed:[bottomTabOptions.sfSymbol withDefault:nil]];
131
+ }
132
+ if (bottomTabOptions.sfSelectedSymbol.hasValue) {
133
+ selectedIcon = [UIImage systemImageNamed:[bottomTabOptions.sfSelectedSymbol withDefault:nil]];
134
+ }
135
+ }
136
+
137
+ if (icon) {
138
+ tabItem.image = [self getIconImage:icon withTint:iconColor];
139
+ }
140
+ if (selectedIcon) {
141
+ tabItem.selectedImage = [self getSelectedIconImage:selectedIcon
142
+ selectedIconColor:selectedIconColor];
143
+ }
144
+
145
+ tabItem.accessibilityIdentifier = [bottomTabOptions.testID withDefault:nil];
146
+ tabItem.accessibilityLabel = [bottomTabOptions.accessibilityLabel withDefault:nil];
147
+ [self appendTitleAttributes:tabItem bottomTabOptions:bottomTabOptions];
148
+
149
+ return tabItem;
150
+ }
151
+
152
+ #pragma mark - Icon helpers
153
+
58
154
  - (UIImage *)getSelectedIconImage:(UIImage *)selectedIcon
59
155
  selectedIconColor:(UIColor *)selectedIconColor {
60
156
  if (selectedIcon) {
@@ -82,6 +178,8 @@
82
178
  return nil;
83
179
  }
84
180
 
181
+ #pragma mark - Title attributes
182
+
85
183
  - (void)appendTitleAttributes:(UITabBarItem *)tabItem
86
184
  bottomTabOptions:(RNNBottomTabOptions *)bottomTabOptions {
87
185
  UIColor *textColor = [bottomTabOptions.textColor withDefault:[UIColor blackColor]];
@@ -71,6 +71,13 @@
71
71
  [button.widthAnchor constraintEqualToConstant:icon.size.width].active = YES;
72
72
  [button.heightAnchor constraintEqualToConstant:icon.size.height].active = YES;
73
73
  self = [super initWithCustomView:button];
74
+ if (@available(iOS 26.0, *)) {
75
+ // The UIButton already paints its own iconBackground chrome, so the
76
+ // iOS 26 shared Platter would double-decorate it. Defaults to YES;
77
+ // callers can opt back in via the hideSharedBackground option.
78
+ self.hidesSharedBackground =
79
+ [buttonOptions.hideSharedBackground withDefault:YES];
80
+ }
74
81
  [self applyOptions:buttonOptions];
75
82
  self.onPress = onPress;
76
83
  return self;
@@ -91,6 +98,25 @@
91
98
  buttonOptions:(RNNButtonOptions *)buttonOptions
92
99
  onPress:(RNNButtonPressCallback)onPress {
93
100
  self = [super initWithCustomView:reactView];
101
+ if (@available(iOS 26.0, *)) {
102
+ // iOS 26 wraps every bar button item in a shared Platter/Liquid-Glass
103
+ // background. React-rendered custom views supply their own chrome, and
104
+ // the Platter both double-decorates and misaligns custom views taller
105
+ // than the standard pill (~30pt). Defaults to YES; callers can opt
106
+ // back in via the hideSharedBackground option.
107
+ self.hidesSharedBackground =
108
+ [buttonOptions.hideSharedBackground withDefault:YES];
109
+ // Pin the custom view to a stable 44pt bar-item size so the navbar
110
+ // reserves the final slot up-front (incl. during push-transition
111
+ // snapshots) and doesn't relayout once React mounts. Without this,
112
+ // React's post-mount sizeToFit changes bounds, the navbar relayouts,
113
+ // and the customView visibly snaps from a 0x0 / wrong-position state
114
+ // to its centered final frame. React content centers within via its
115
+ // own flex layout.
116
+ reactView.translatesAutoresizingMaskIntoConstraints = NO;
117
+ [reactView.widthAnchor constraintEqualToConstant:44].active = YES;
118
+ [reactView.heightAnchor constraintEqualToConstant:44].active = YES;
119
+ }
94
120
  [self applyOptions:buttonOptions];
95
121
  self.onPress = onPress;
96
122
  return self;
@@ -56,6 +56,7 @@
56
56
  if (_presentedViewController &&
57
57
  ![navigationController.viewControllers containsObject:_presentedViewController]) {
58
58
  [_presentedViewController screenPopped];
59
+ [_presentedViewController destroyReactView];
59
60
  _isPopping = NO;
60
61
  }
61
62