@apexnative/navigation 1.0.0-alpha.7 → 1.0.0-alpha.9

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 (53) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/android/src/main/java/dev/apexnative/navigation/options/SideMenuOptions.kt +6 -1
  3. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +36 -5
  4. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -0
  5. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +35 -3
  6. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +50 -1
  7. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarVisibility.kt +9 -2
  8. package/android/src/main/java/dev/apexnative/navigation/views/sidemenu/SideMenuView.kt +12 -3
  9. package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +34 -4
  10. package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +6 -3
  11. package/docs-md/containers/side-menu.md +15 -0
  12. package/docs-md/containers/tabs.md +1 -1
  13. package/docs-md/index.json +4 -0
  14. package/ios/CLAUDE.md +8 -2
  15. package/ios/CORE.md +2 -2
  16. package/ios/Core/ANNLayoutProtocol.h +5 -5
  17. package/ios/Core/ANNParking.h +5 -4
  18. package/ios/Core/ANNReloadCoordinator.mm +1 -4
  19. package/ios/Core/ANNRenderWait.h +5 -5
  20. package/ios/Core/ANNRenderWait.mm +24 -39
  21. package/ios/Core/ANNUtils.h +7 -0
  22. package/ios/Core/ANNUtils.mm +10 -0
  23. package/ios/Core/ApexNavigation.mm +1 -4
  24. package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
  25. package/ios/Layouts/Overlay/ANNOverlayCoordinator.mm +2 -8
  26. package/ios/Layouts/Root/ANNRootCoordinator.mm +1 -1
  27. package/ios/Layouts/Screen/ANNComponentViewController.h +9 -15
  28. package/ios/Layouts/Screen/ANNComponentViewController.mm +6 -42
  29. package/ios/Layouts/Screen/ANNScreenPlace.h +12 -0
  30. package/ios/Layouts/Screen/ANNScreenPlace.mm +46 -16
  31. package/ios/Layouts/SideMenu/ANNSideMenuTypes.h +8 -0
  32. package/ios/Layouts/SideMenu/ANNSideMenuViewController.mm +83 -14
  33. package/ios/Layouts/Stack/ANNStackController.mm +5 -2
  34. package/ios/Layouts/Stack/ANNStackQueue.h +2 -1
  35. package/ios/Layouts/Tabs/ANNBottomTabsController.h +3 -0
  36. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +222 -51
  37. package/ios/Options/ANNSideMenuOptions.h +2 -0
  38. package/ios/Options/ANNSideMenuOptions.mm +2 -0
  39. package/ios/React/ANNSafeAreaViewComponentView.mm +2 -2
  40. package/ios/React/Slots/ANNSlotHost.mm +1 -3
  41. package/lib/module/api/screens/registerScreens.js +26 -5
  42. package/lib/module/api/screens/registerScreens.js.map +1 -1
  43. package/lib/typescript/api/screens/registerScreens.d.ts.map +1 -1
  44. package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -1
  45. package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
  46. package/lib/typescript/interfaces/options/sideMenu.d.ts +16 -0
  47. package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/src/api/screens/registerScreens.tsx +17 -2
  50. package/src/interfaces/options/bottomTabs.ts +2 -1
  51. package/src/interfaces/options/sideMenu.ts +17 -0
  52. package/ios/Layouts/Tabs/ANNTabsPreloadWindow.h +0 -27
  53. package/ios/Layouts/Tabs/ANNTabsPreloadWindow.mm +0 -55
@@ -12,6 +12,24 @@ static const CGFloat ANNSideMenuDimAlpha = 0.5;
12
12
  static const CGFloat ANNSideMenuMaxStretch = 1.1;
13
13
  static const NSTimeInterval ANNSideMenuMinimumDuration = 0.1;
14
14
 
15
+ // `sideMenu.menusAttachMode` of the side menu's own options, else of the defaults; any other value, and none, is
16
+ // `together`.
17
+ static ANNSideMenuAttachMode ANNSideMenuAttachModeOf(ANNNavigationOptions *options,
18
+ ANNNavigationOptions *defaultOptions) {
19
+ Text *value = options.sideMenu.menusAttachMode;
20
+ if (!value.hasValue) {
21
+ value = defaultOptions.sideMenu.menusAttachMode;
22
+ }
23
+ NSString *mode = [value withDefault:nil];
24
+ if ([mode isEqualToString:@"afterCenter"]) {
25
+ return ANNSideMenuAttachModeAfterCenter;
26
+ }
27
+ if ([mode isEqualToString:@"onOpen"]) {
28
+ return ANNSideMenuAttachModeOnOpen;
29
+ }
30
+ return ANNSideMenuAttachModeTogether;
31
+ }
32
+
15
33
  static UINavigationController *ANNStackOfResponder(UIResponder *responder) {
16
34
  for (; responder; responder = responder.nextResponder) {
17
35
  if ([responder isKindOfClass:UINavigationController.class]) {
@@ -65,6 +83,8 @@ typedef NS_ENUM(NSInteger, ANNSideMenuAppearance) {
65
83
  @property(nonatomic) BOOL glass;
66
84
  @property(nonatomic) ANNSideMenuAppearance appearance;
67
85
  @property(nonatomic, copy) NSString *scrimTestID;
86
+ // The menu's part is in its container and renders (`menusAttachMode`).
87
+ @property(nonatomic) BOOL attached;
68
88
  @end
69
89
 
70
90
  @implementation ANNSideMenuSideState
@@ -95,6 +115,8 @@ typedef NS_ENUM(NSInteger, ANNSideMenuAppearance) {
95
115
  ANNSideMenuSideState *_leftState;
96
116
  ANNSideMenuSideState *_rightState;
97
117
  ANNSideMenuOpenGestureMode _openGestureMode;
118
+ ANNSideMenuAttachMode _attachMode;
119
+ BOOL _firstAppeared;
98
120
  NSString *_animationType;
99
121
  UIView *_centerContainer;
100
122
  UIView *_leftContainer;
@@ -132,6 +154,7 @@ typedef NS_ENUM(NSInteger, ANNSideMenuAppearance) {
132
154
  _leftState = [ANNSideMenuSideState new];
133
155
  _rightState = [ANNSideMenuSideState new];
134
156
  _openGestureMode = ANNSideMenuOpenGestureModeBezel;
157
+ _attachMode = ANNSideMenuAttachModeOf(options, defaultOptions);
135
158
  [self setSideMenuChildren:childViewControllers];
136
159
 
137
160
  self.presenter = presenter;
@@ -181,10 +204,15 @@ typedef NS_ENUM(NSInteger, ANNSideMenuAppearance) {
181
204
 
182
205
  - (void)viewDidLoad {
183
206
  [super viewDidLoad];
184
- _leftContainer = [self containerForChild:self.left];
185
- _rightContainer = [self containerForChild:self.right];
186
- _centerContainer = [self containerForChild:self.center];
207
+ _leftContainer = [self makeContainer];
208
+ _rightContainer = [self makeContainer];
209
+ _centerContainer = [self makeContainer];
210
+ [self putChild:self.center into:_centerContainer];
187
211
  _centerContainer.hidden = NO;
212
+ if (_attachMode == ANNSideMenuAttachModeTogether) {
213
+ [self attachMenu:ANNSideMenuSideLeft];
214
+ [self attachMenu:ANNSideMenuSideRight];
215
+ }
188
216
  [self applyGlassOf:ANNSideMenuSideLeft];
189
217
  [self applyGlassOf:ANNSideMenuSideRight];
190
218
 
@@ -200,32 +228,65 @@ typedef NS_ENUM(NSInteger, ANNSideMenuAppearance) {
200
228
  [self.view addGestureRecognizer:_pan];
201
229
  }
202
230
 
203
- // A child in a view of its own, which the menu moves; a menu's container is hidden while closed.
204
- - (UIView *)containerForChild:(UIViewController *)child {
231
+ // The view of a part, which the menu moves; a menu's container is hidden while closed.
232
+ - (UIView *)makeContainer {
205
233
  UIView *container = [[UIView alloc] initWithFrame:self.view.bounds];
206
234
  container.hidden = YES;
207
235
  [self.view addSubview:container];
208
- if (child) {
209
- [self addChildViewController:child];
210
- child.view.frame = container.bounds;
211
- child.view.autoresizingMask =
212
- UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
213
- [container addSubview:child.view];
214
- [child didMoveToParentViewController:self];
215
- }
216
236
  return container;
217
237
  }
218
238
 
239
+ // A part in its container, over the container's glass. Loading the part's view loads its node's, which renders.
240
+ - (void)putChild:(UIViewController *)child into:(UIView *)container {
241
+ if (!child) {
242
+ return;
243
+ }
244
+ [self addChildViewController:child];
245
+ child.view.frame = container.bounds;
246
+ child.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
247
+ [container addSubview:child.view];
248
+ [child didMoveToParentViewController:self];
249
+ }
250
+
251
+ // The menu's part goes into its container and renders, once: with the side menu's view (`together`), after its first
252
+ // appearance (`afterCenter`), or as the menu first starts to open (`onOpen`, and any mode for a menu that opens
253
+ // earlier). A menu not attached yet keeps its node and options; only its view and content wait.
254
+ - (void)attachMenu:(ANNSideMenuSide)side {
255
+ ANNSideMenuChildViewController *menu = [self menuOf:side];
256
+ if (!menu || [self stateOf:side].attached || self.nodeHelper.destroyed) {
257
+ return;
258
+ }
259
+ [self loadViewIfNeeded];
260
+ ANNSideMenuSideState *state = [self stateOf:side];
261
+ if (state.attached) {
262
+ return;
263
+ }
264
+ state.attached = YES;
265
+ [self putChild:menu into:[self containerOf:side]];
266
+ [menu render];
267
+ }
268
+
269
+ // The current part renders, and every other part that is attached or will be with the side menu's view.
219
270
  - (void)render {
220
271
  [self.nodeHelper render];
221
272
  UIViewController *currentChild = self.getCurrentChild;
222
273
  for (UIViewController *vc in _sideMenuChildren) {
223
- if (vc != currentChild) {
274
+ if (vc != currentChild && [self rendersPart:vc]) {
224
275
  [vc render];
225
276
  }
226
277
  }
227
278
  }
228
279
 
280
+ - (BOOL)rendersPart:(UIViewController *)part {
281
+ if (part == self.left) {
282
+ return _attachMode == ANNSideMenuAttachModeTogether || _leftState.attached;
283
+ }
284
+ if (part == self.right) {
285
+ return _attachMode == ANNSideMenuAttachModeTogether || _rightState.attached;
286
+ }
287
+ return YES;
288
+ }
289
+
229
290
  #pragma mark - Options
230
291
 
231
292
  - (ANNSideMenuSideState *)stateOf:(ANNSideMenuSide)side {
@@ -380,6 +441,7 @@ typedef NS_ENUM(NSInteger, ANNSideMenuAppearance) {
380
441
 
381
442
  // The menu on screen in its place under or over the center, its screens about to appear.
382
443
  - (void)show:(ANNSideMenuSide)side {
444
+ [self attachMenu:side];
383
445
  _side = side;
384
446
  [self orderSide:side];
385
447
  [self containerOf:side].hidden = NO;
@@ -586,6 +648,13 @@ typedef NS_ENUM(NSInteger, ANNSideMenuAppearance) {
586
648
  if (_side != ANNSideMenuSideNone) {
587
649
  [self side:_side transitionEnded:YES];
588
650
  }
651
+ if (!_firstAppeared) {
652
+ _firstAppeared = YES;
653
+ if (_attachMode == ANNSideMenuAttachModeAfterCenter) {
654
+ [self attachMenu:ANNSideMenuSideLeft];
655
+ [self attachMenu:ANNSideMenuSideRight];
656
+ }
657
+ }
589
658
  }
590
659
 
591
660
  - (void)viewWillDisappear:(BOOL)animated {
@@ -3,6 +3,7 @@
3
3
  #import "ANNAppDirection.h"
4
4
  #import "ANNAppearanceDispatcher.h"
5
5
  #import "ANNAssert.h"
6
+ #import "ANNBottomTabsController.h"
6
7
  #import "ANNComponentViewController.h"
7
8
  #import "ANNElementFinder.h"
8
9
  #import "ANNErrorTexts.h"
@@ -1175,10 +1176,12 @@ static const CGFloat ANNOwnSwipeEdgeWidth = 20;
1175
1176
  return self;
1176
1177
  }
1177
1178
 
1178
- // In More the More navigation is what shows the screens.
1179
+ // In More the More navigation is what shows the screens. A tab rendered before it shows is in a window while its
1180
+ // screens render there, and not on screen.
1179
1181
  - (BOOL)stackIsOnScreen {
1180
1182
  return self.shownNavigationController.viewIfLoaded.window != nil &&
1181
- ![ANNPendingLayers isWaitingToShow:self];
1183
+ ![ANNPendingLayers isWaitingToShow:self] &&
1184
+ ![ANNBottomTabsController isInUnseenTab:self];
1182
1185
  }
1183
1186
 
1184
1187
  // Only the stack's own transitions (a push, a pop, a replace of its view controllers, the interactive pop): a
@@ -60,7 +60,8 @@ typedef NS_ENUM(NSInteger, ANNStackChange) {
60
60
  /// The stack's own view controller: the layout parent of the nodes it took and doesn't show yet.
61
61
  - (UIViewController *)stackViewController;
62
62
 
63
- /// The stack is seen: its view is in a window, and it isn't inside a layer that waits to show (`ANNPendingLayers`).
63
+ /// The stack is seen: its view is in a window, and it isn't inside a layer that waits to show (`ANNPendingLayers`) nor
64
+ /// in a tab that renders before it shows (`isInUnseenTab:` of the tabs).
64
65
  - (BOOL)stackIsOnScreen;
65
66
 
66
67
  /// A transition UIKit runs on the stack now (one the queue didn't start); nil when none runs.
@@ -45,6 +45,9 @@ typedef NS_ENUM(NSInteger, ANNTabPressSource) {
45
45
  /// The nearest tabs among the layout parents of `node` (not `node` itself); nil out of tabs.
46
46
  + (nullable ANNBottomTabsController *)tabsOf:(UIViewController *)node;
47
47
 
48
+ /// `node` is in a tab rendered before it shows, which renders its content in its tabs' view: in a window, not shown.
49
+ + (BOOL)isInUnseenTab:(UIViewController *)node;
50
+
48
51
  /// The tabs in layout order.
49
52
  @property(nonatomic, copy, readonly) NSArray<UIViewController *> *layoutTabs;
50
53
 
@@ -7,15 +7,17 @@
7
7
  #import "ANNMoreNavigationDelegate.h"
8
8
  #import "ANNNavigationState.h"
9
9
  #import "ANNNodeRegistry.h"
10
+ #import "ANNParking.h"
10
11
  #import "ANNPendingLayers.h"
11
12
  #import "ANNScreenBars.h"
13
+ #import "ANNScreenPlace.h"
12
14
  #import "ANNScrollToTop.h"
13
15
  #import "ANNShownScreens.h"
14
16
  #import "ANNStackController.h"
17
+ #import "ANNStackQueue.h"
15
18
  #import "ANNSystemUI.h"
16
19
  #import "ANNTabLongPress.h"
17
20
  #import "ANNTabsDesign.h"
18
- #import "ANNTabsPreloadWindow.h"
19
21
  #import "InteractivePopGestureDelegate.h"
20
22
  #import <React/RCTLog.h>
21
23
 
@@ -23,13 +25,17 @@
23
25
  // waits for it; the drive only must not stay).
24
26
  static const NSTimeInterval ANNSwitchDriveLimit = 2;
25
27
 
26
- // How long a step of `afterInitialTab` waits for its tab's root to mount before the next one starts, and how long a
27
- // lent React view stays in the preload window at most without mounting.
28
+ // How long a step of `afterInitialTab` waits for its tab's root to mount before the next one starts.
28
29
  static const NSTimeInterval ANNTabMountLimit = 0.5;
29
30
 
30
31
  // The key of the tabs' ready handlers on the screens of their selected tab (`afterInitialTab`).
31
32
  static NSString *const ANNTabsReadyKey = @"ApexNavigation.tabs";
32
33
 
34
+ // The key of the tabs' ready handlers on the screens of a parked tab, and the longest such a tab waits for them in the
35
+ // parking: past it, its screens render their content where the tab shows.
36
+ static NSString *const ANNTabsUnseenKey = @"ApexNavigation.tabs.unseen";
37
+ static const NSTimeInterval ANNUnseenRenderLimit = 5;
38
+
33
39
  // Whether UIKit pops the stack of a tab tapped again itself: the core answers "select" and lets it. NO would make the
34
40
  // core pop it for every source and answer "don't" (the simulator decides).
35
41
  static const BOOL ANNUIKitPopsOnRepeatPress = YES;
@@ -81,8 +87,12 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
81
87
  // The `UITab` of each tab, in layout order (iOS 18 and later).
82
88
  NSArray *_uiTabs;
83
89
  NSHashTable<UIViewController *> *_renderedTabs;
84
- ANNTabsPreloadWindow *_preloadWindow;
85
- NSHashTable<ANNComponentViewController *> *_lentScreens;
90
+ // The tabs rendered before they show (`together`, the chain of `afterInitialTab`) wait unseen and hidden in the
91
+ // tabs' view until their screens rendered their content, as on Android: a screen's safe area view lays out only in
92
+ // a window. They leave it then, since the core takes a view in a window for one that shows, and their screens keep
93
+ // the place they rendered at until they appear. Those not parked yet wait for the tabs' view in a window.
94
+ ANNParking *_unseenParking;
95
+ NSHashTable<UIViewController *> *_tabsToPark;
86
96
  // The chain of `afterInitialTab`: started once; a destroy ends it by its number.
87
97
  BOOL _chainStarted;
88
98
  NSUInteger _chainNumber;
@@ -137,7 +147,8 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
137
147
  if (self) {
138
148
  _tabs = [childViewControllers copy] ?: @[];
139
149
  _renderedTabs = [NSHashTable weakObjectsHashTable];
140
- _lentScreens = [NSHashTable weakObjectsHashTable];
150
+ _unseenParking = [ANNParking new];
151
+ _tabsToPark = [NSHashTable weakObjectsHashTable];
141
152
  _staleTabs = [NSHashTable weakObjectsHashTable];
142
153
  _pressedIndex = NSNotFound;
143
154
  _codeChoiceIndex = NSNotFound;
@@ -444,7 +455,7 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
444
455
  }
445
456
 
446
457
  // The selected tab's waited screens by the command's rule; with `together` the top screen of every other tab too, by
447
- // the first mount of its root (More's tabs too).
458
+ // its content rendered unseen (More's tabs too).
448
459
  - (NSArray<UIViewController *> *)waitedComponentViewControllers {
449
460
  NSMutableArray<UIViewController *> *screens =
450
461
  [[[self selectedTabNode] waitedComponentViewControllers] mutableCopy]
@@ -464,7 +475,7 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
464
475
 
465
476
  - (ANNWaitedReadiness)waitedReadinessOfScreen:(UIViewController *)screen {
466
477
  NSUInteger index = [self indexOfTabHolding:screen];
467
- return index != NSNotFound && index != _selectedTabIndex ? ANNWaitedReadinessRootMounted
478
+ return index != NSNotFound && index != _selectedTabIndex ? ANNWaitedReadinessUnseen
468
479
  : ANNWaitedReadinessContent;
469
480
  }
470
481
 
@@ -474,12 +485,12 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
474
485
  if (self.nodeHelper.destroyed) {
475
486
  return;
476
487
  }
477
- [self renderTabAtIndex:_selectedTabIndex preloading:NO];
488
+ [self renderTabAtIndex:_selectedTabIndex unseen:NO];
478
489
  switch (_attachMode) {
479
490
  case ANNTabsAttachModeTogether:
480
491
  for (NSUInteger index = 0; index < _tabs.count; index++) {
481
492
  if (index != _selectedTabIndex) {
482
- [self renderTabAtIndex:index preloading:YES];
493
+ [self renderTabAtIndex:index unseen:YES];
483
494
  }
484
495
  }
485
496
  break;
@@ -561,6 +572,10 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
561
572
  }
562
573
  [_longPress detach];
563
574
  [self.nodeHelper unregisterNode];
575
+ [_tabsToPark removeAllObjects];
576
+ for (UIViewController *tab in _unseenParking.parkedNodes) {
577
+ [self unparkTab:tab showing:NO];
578
+ }
564
579
  for (UIViewController *tab in _tabs) {
565
580
  @try {
566
581
  [tab destroy];
@@ -569,7 +584,6 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
569
584
  tab.layoutInfo.componentId ?: @"", exception.reason ?: exception.name);
570
585
  }
571
586
  }
572
- [self closePreloadWindow];
573
587
  [[self tabsPresenter] tabsWillGo];
574
588
  [_components stopAll];
575
589
  }
@@ -591,6 +605,13 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
591
605
  [[self tabsPresenter] barDidLayOutInWindow];
592
606
  }
593
607
  [[self tabsPresenter] tabsDidLayOut];
608
+ // A tab UIKit shows by a way of its own leaves the parking too.
609
+ UIViewController *shown = self.selectedViewController;
610
+ if (shown && [_unseenParking isParked:shown]) {
611
+ [self unparkTab:shown showing:YES];
612
+ }
613
+ [self layOutParkedTabs];
614
+ [self parkUnseenTabs];
594
615
  }
595
616
 
596
617
  - (void)viewWillAppear:(BOOL)animated {
@@ -605,9 +626,10 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
605
626
  [[self tabsPresenter] tabsDidAppear];
606
627
  }
607
628
 
608
- #pragma mark - Rendering and the preload window
629
+ #pragma mark - Rendering
609
630
 
610
- - (void)renderTabAtIndex:(NSUInteger)index preloading:(BOOL)preloading {
631
+ // A tab renders once. `unseen`: it renders before it shows, and waits in the parking until then.
632
+ - (void)renderTabAtIndex:(NSUInteger)index unseen:(BOOL)unseen {
611
633
  if (index >= _tabs.count) {
612
634
  return;
613
635
  }
@@ -617,62 +639,202 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
617
639
  }
618
640
  [_renderedTabs addObject:tab];
619
641
  [tab render];
620
- if (preloading) {
621
- [self lendTopOfTabAtIndex:index];
642
+ if (unseen) {
643
+ [_tabsToPark addObject:tab];
644
+ [self parkUnseenTabs];
622
645
  }
623
646
  }
624
647
 
625
- // The React view of the tab's top screen mounts in the preload window and comes back into its screen at its mount or
626
- // when its tab first shows, whichever comes first; a native screen stays where it is.
627
- - (void)lendTopOfTabAtIndex:(NSUInteger)index {
628
- UIViewController *node = [self screenOfTabAtIndex:index];
629
- if (![node isKindOfClass:ANNComponentViewController.class]) {
648
+ // The tabs rendered unseen go into the parking once the tabs' view is in a window, at the back of it and hidden, so
649
+ // that a switch's transition never shows them; the shown tab never, nor a tab with nothing to render (native screens).
650
+ - (void)parkUnseenTabs {
651
+ if (_tabsToPark.count == 0 || !self.viewIfLoaded.window || self.nodeHelper.destroyed) {
630
652
  return;
631
653
  }
632
- ANNComponentViewController *screen = (ANNComponentViewController *)node;
633
- UIView<ANNComponentProtocol> *content = screen.reactView;
634
- if (![content respondsToSelector:@selector(whenMounted:)] || content.hasMounted) {
635
- return;
654
+ UIViewController *shown = self.selectedViewController;
655
+ UIViewController *selected = [self selectedTabNode];
656
+ for (UIViewController *tab in _tabsToPark.allObjects) {
657
+ [_tabsToPark removeObject:tab];
658
+ if (tab == shown || tab == selected || tab.viewIfLoaded.window ||
659
+ [_unseenParking isParked:tab]) {
660
+ continue;
661
+ }
662
+ NSArray<ANNComponentViewController *> *screens = [self unrenderedScreensOfTab:tab];
663
+ if (screens.count == 0) {
664
+ continue;
665
+ }
666
+ [_unseenParking park:tab inView:self.view];
667
+ tab.view.hidden = YES;
668
+ [self layOutParkedTabs];
669
+ [self placeScreensOfParkedTab:tab shownFirst:screens];
670
+ [self unparkTab:tab whenScreensRender:screens];
636
671
  }
637
- if (!_preloadWindow) {
638
- _preloadWindow = [ANNTabsPreloadWindow preloadWindow];
672
+ }
673
+
674
+ // The screens the tab shows first whose content didn't render yet.
675
+ - (NSArray<ANNComponentViewController *> *)unrenderedScreensOfTab:(UIViewController *)tab {
676
+ NSMutableArray<ANNComponentViewController *> *screens = [NSMutableArray new];
677
+ for (UIViewController *node in [tab waitedComponentViewControllers]) {
678
+ if (![node isKindOfClass:ANNComponentViewController.class]) {
679
+ continue;
680
+ }
681
+ ANNComponentViewController *screen = (ANNComponentViewController *)node;
682
+ if (screen.reactView && !screen.reactView.isReady) {
683
+ [screens addObject:screen];
684
+ }
639
685
  }
640
- if (![screen lendContentTo:_preloadWindow]) {
686
+ return screens;
687
+ }
688
+
689
+ // A parked tab lays out where the shown tab is: beside the sidebar on iPad too.
690
+ - (void)layOutParkedTabs {
691
+ UIView *shownView = self.selectedViewController.viewIfLoaded;
692
+ if (!shownView.window) {
641
693
  return;
642
694
  }
643
- [_lentScreens addObject:screen];
695
+ CGRect frame = [self.view convertRect:shownView.bounds fromView:shownView];
696
+ for (UIViewController *tab in _unseenParking.parkedNodes) {
697
+ UIView *view = tab.viewIfLoaded;
698
+ if (view && !CGRectEqualToRect(view.frame, frame)) {
699
+ view.frame = frame;
700
+ }
701
+ }
702
+ }
703
+
704
+ // UIKit gives the views of a tab not shown no safe area: the screens on top of its stacks lay out with their place
705
+ // there, measured in the view of what the tabs show (`ANNScreenPlace`), the safe area and bars they get as the tab
706
+ // shows. A screen the tab shows first keeps the place it rendered at until it appears: as the tab shows, UIKit first
707
+ // gives it a part of its safe area (the bar without the status bar). The others (a closed side menu's) keep it until
708
+ // UIKit gives them a safe area of their own.
709
+ - (void)placeScreensOfParkedTab:(UIViewController *)tab
710
+ shownFirst:(NSArray<ANNComponentViewController *> *)shownFirst {
711
+ NSHashTable<ANNComponentViewController *> *placed = [NSHashTable weakObjectsHashTable];
712
+ NSMutableArray<ANNStackController *> *stacks = [NSMutableArray new];
713
+ ANNStacksOfNode(tab, stacks);
714
+ for (ANNStackController *stack in stacks) {
715
+ UIViewController *top = stack.topViewController;
716
+ for (ANNComponentViewController *screen in top ? ANNStackScreensOfEntry(top) : @[]) {
717
+ [self placeScreen:screen
718
+ onTop:top
719
+ ofStack:stack
720
+ ofParkedTab:tab
721
+ kept:[shownFirst containsObject:screen]];
722
+ [placed addObject:screen];
723
+ }
724
+ }
725
+ for (ANNComponentViewController *screen in shownFirst) {
726
+ if (![placed containsObject:screen]) {
727
+ [self placeScreen:screen onTop:nil ofStack:nil ofParkedTab:tab kept:YES];
728
+ }
729
+ }
730
+ }
731
+
732
+ // The place of one screen of a parked tab: on top of its stack, or a screen of its own. A screen covered since it
733
+ // rendered (a push onto its stack before the tab showed) lays out with its own safe area.
734
+ - (void)placeScreen:(ANNComponentViewController *)screen
735
+ onTop:(nullable UIViewController *)top
736
+ ofStack:(nullable UINavigationController *)stack
737
+ ofParkedTab:(UIViewController *)tab
738
+ kept:(BOOL)kept {
644
739
  __weak ANNBottomTabsController *weakSelf = self;
740
+ __weak UIViewController *weakTab = tab;
645
741
  __weak ANNComponentViewController *weakScreen = screen;
646
- [content whenMounted:^{
647
- [weakSelf takeBack:weakScreen];
648
- }];
649
- // A root that never mounts goes back anyway: it renders where its tab shows.
650
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(ANNTabMountLimit * 2 * NSEC_PER_SEC)),
742
+ __weak UINavigationController *weakStack = stack;
743
+ __weak UIViewController *weakTop = top;
744
+ BOOL inStack = stack != nil;
745
+ __block ANNScreenPlace *last = nil;
746
+ screen.placeWhileParked = ^ANNScreenPlace * {
747
+ ANNComponentViewController *placed = weakScreen;
748
+ UINavigationController *placedStack = weakStack;
749
+ UIViewController *entry = weakTop;
750
+ if (inStack && (!placedStack || !entry || placedStack.topViewController != entry)) {
751
+ return nil;
752
+ }
753
+ UIView *host = [weakSelf placeHostForTab:weakTab];
754
+ if (!placed || !host) {
755
+ return last;
756
+ }
757
+ ANNScreenPlace *measured =
758
+ inStack ? [ANNScreenPlace placeOfEntry:entry
759
+ inStack:placedStack
760
+ measuredIn:host
761
+ options:[ANNStackOptionsScreen(entry)
762
+ resolveOptionsWithDefault]]
763
+ : [ANNScreenPlace placeOfScreen:placed measuredIn:host];
764
+ if (measured) {
765
+ last = measured;
766
+ }
767
+ return last;
768
+ };
769
+ screen.placeKeptUntilAppearance = kept;
770
+ [screen updateRootLayout];
771
+ }
772
+
773
+ // Where the screens of a tab rendered unseen measure their place: the view of what the tabs show now (UIKit's More
774
+ // navigation too). Nil once the tab itself shows.
775
+ - (nullable UIView *)placeHostForTab:(UIViewController *)tab {
776
+ UIViewController *shown = self.selectedViewController;
777
+ UIView *host = shown.viewIfLoaded;
778
+ UIView *tabView = tab.viewIfLoaded;
779
+ if (!tab || !host.window || shown == tab || [self selectedTabNode] == tab ||
780
+ (tabView && [host isDescendantOfView:tabView])) {
781
+ return nil;
782
+ }
783
+ return host;
784
+ }
785
+
786
+ // The parked tab leaves the parking once every screen it waits for rendered its content, or at the limit.
787
+ - (void)unparkTab:(UIViewController *)tab
788
+ whenScreensRender:(NSArray<ANNComponentViewController *> *)screens {
789
+ __weak ANNBottomTabsController *weakSelf = self;
790
+ __weak UIViewController *weakTab = tab;
791
+ NSHashTable<ANNComponentViewController *> *waited = [NSHashTable weakObjectsHashTable];
792
+ for (ANNComponentViewController *screen in screens) {
793
+ [waited addObject:screen];
794
+ }
795
+ for (ANNComponentViewController *screen in screens) {
796
+ [screen
797
+ setReadyHandler:^{
798
+ for (ANNComponentViewController *other in waited.allObjects) {
799
+ if (other.reactView && !other.reactView.isReady) {
800
+ return;
801
+ }
802
+ }
803
+ [weakSelf unparkTab:weakTab showing:NO];
804
+ }
805
+ forKey:ANNTabsUnseenKey];
806
+ }
807
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(ANNUnseenRenderLimit * NSEC_PER_SEC)),
651
808
  dispatch_get_main_queue(), ^{
652
- [weakSelf takeBack:weakScreen];
809
+ [weakSelf unparkTab:weakTab showing:NO];
653
810
  });
654
811
  }
655
812
 
656
- - (void)takeBack:(ANNComponentViewController *)screen {
657
- if (screen) {
658
- [screen takeContentBack];
659
- [_lentScreens removeObject:screen];
813
+ // The tab rendered, is about to show, or the tabs go: it leaves the parking, and UIKit places its view as it shows it.
814
+ // `showing`: it shows in this turn, so UIKit's departure of a tab without a parent (More's, with the tab API) ends
815
+ // here.
816
+ - (void)unparkTab:(UIViewController *)tab showing:(BOOL)showing {
817
+ if (!tab) {
818
+ return;
660
819
  }
661
- for (ANNComponentViewController *lent in _lentScreens.allObjects) {
662
- if (lent.contentIsLent) {
663
- return;
664
- }
820
+ [_tabsToPark removeObject:tab];
821
+ if (![_unseenParking isParked:tab]) {
822
+ return;
665
823
  }
666
- [self closePreloadWindow];
824
+ tab.viewIfLoaded.hidden = NO;
825
+ [_unseenParking unpark:tab endingDeparture:showing && !tab.parentViewController];
667
826
  }
668
827
 
669
- - (void)closePreloadWindow {
670
- for (ANNComponentViewController *screen in _lentScreens.allObjects) {
671
- [screen takeContentBack];
828
+ + (BOOL)isInUnseenTab:(UIViewController *)node {
829
+ UIViewController *child = node;
830
+ for (UIViewController *parent = node.layoutParent; parent;
831
+ child = parent, parent = parent.layoutParent) {
832
+ if ([parent isKindOfClass:ANNBottomTabsController.class] &&
833
+ [((ANNBottomTabsController *)parent)->_unseenParking isParked:child]) {
834
+ return YES;
835
+ }
672
836
  }
673
- [_lentScreens removeAllObjects];
674
- [_preloadWindow close];
675
- _preloadWindow = nil;
837
+ return NO;
676
838
  }
677
839
 
678
840
  // `afterInitialTab`: the selected tab's shown screens ready (by their content), then the others one by one.
@@ -739,7 +901,7 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
739
901
  if (next == NSNotFound) {
740
902
  return;
741
903
  }
742
- [self renderTabAtIndex:next preloading:YES];
904
+ [self renderTabAtIndex:next unseen:YES];
743
905
  __block BOOL stepped = NO;
744
906
  __weak ANNBottomTabsController *weakSelf = self;
745
907
  dispatch_block_t step = ^{
@@ -769,8 +931,17 @@ static void ANNStacksOfNode(UIViewController *node, NSMutableArray<ANNStackContr
769
931
  if (index >= _tabs.count) {
770
932
  return;
771
933
  }
772
- [self renderTabAtIndex:index preloading:NO];
934
+ [self renderTabAtIndex:index unseen:NO];
773
935
  UIViewController *tab = _tabs[index];
936
+ // Its screens rendered unseen measure their place once more while the tab that shows now is still the host: the
937
+ // device may have turned since, or the shown tab laid out again.
938
+ for (UIViewController *node in [tab waitedComponentViewControllers]) {
939
+ if ([node isKindOfClass:ANNComponentViewController.class] &&
940
+ ((ANNComponentViewController *)node).placeKeptUntilAppearance) {
941
+ [(ANNComponentViewController *)node updateRootLayout];
942
+ }
943
+ }
944
+ [self unparkTab:tab showing:YES];
774
945
  if ([_staleTabs containsObject:tab]) {
775
946
  [_staleTabs removeObject:tab];
776
947
  [ANNBottomTabsPresenter repaintNode:tab];
@@ -12,6 +12,8 @@ NS_ASSUME_NONNULL_BEGIN
12
12
  @property(nonatomic, strong) Text *openGestureMode;
13
13
  /// `'parallax'`, `'slide'`, `'slide-and-scale'` or `'door'`.
14
14
  @property(nonatomic, strong) Text *animationType;
15
+ /// `'together'`, `'afterCenter'` or `'onOpen'`.
16
+ @property(nonatomic, strong) Text *menusAttachMode;
15
17
  @property(nonatomic, strong) Text *testID;
16
18
 
17
19
  @end
@@ -9,6 +9,8 @@
9
9
  [ANNOptionField enumField:@"openGestureMode" values:@[ @"bezel", @"entireScreen" ]],
10
10
  [ANNOptionField enumField:@"animationType"
11
11
  values:@[ @"parallax", @"slide", @"slide-and-scale", @"door" ]],
12
+ [ANNOptionField enumField:@"menusAttachMode"
13
+ values:@[ @"together", @"afterCenter", @"onOpen" ]],
12
14
  [ANNOptionField textField:@"testID"],
13
15
  ];
14
16
  }
@@ -128,8 +128,8 @@ using namespace facebook::react;
128
128
 
129
129
  // The screen renders its content once this view is laid out in its place with a size, as
130
130
  // react-native-safe-area-context does: a list that is there before the first layout of the top bar
131
- // makes UIKit start the large title collapsed. The preload window of tabs attached together has no
132
- // size: a tab renders its content where it shows.
131
+ // makes UIKit start the large title collapsed. A tab rendered before it shows lays out in the
132
+ // tabs' view, unseen, and renders its content there.
133
133
  - (void)sendReadyIfLaidOut {
134
134
  if (_readySent || _eventEmitter == nullptr || self.superview == nil) {
135
135
  return;
@@ -35,9 +35,7 @@ static RCTFabricSurface *ANNSlotHostSurface;
35
35
 
36
36
  // Hidden and without a root view controller: it takes no touches and has no say in the orientation or the status
37
37
  // bar (a visible window above the app does). The tree still mounts; the slots move their content into screens.
38
- UIWindowScene *scene = ANNUtils.mainWindow.windowScene;
39
- UIWindow *window = scene ? [[UIWindow alloc] initWithWindowScene:scene]
40
- : [[UIWindow alloc] initWithFrame:ANNUtils.mainScreen.bounds];
38
+ UIWindow *window = [ANNUtils makeWindowOfClass:UIWindow.class];
41
39
  surfaceView.frame = window.bounds;
42
40
  [window addSubview:surfaceView];
43
41
  window.hidden = YES;