@apexnative/navigation 1.0.0-alpha.8 → 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 (44) hide show
  1. package/CHANGELOG.md +13 -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/views/sidemenu/SideMenuView.kt +12 -3
  7. package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +34 -4
  8. package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +6 -3
  9. package/docs-md/containers/side-menu.md +15 -0
  10. package/docs-md/containers/tabs.md +1 -1
  11. package/docs-md/index.json +4 -0
  12. package/ios/CLAUDE.md +4 -1
  13. package/ios/CORE.md +2 -2
  14. package/ios/Core/ANNLayoutProtocol.h +5 -5
  15. package/ios/Core/ANNParking.h +5 -4
  16. package/ios/Core/ANNReloadCoordinator.mm +1 -4
  17. package/ios/Core/ANNRenderWait.h +5 -5
  18. package/ios/Core/ANNRenderWait.mm +24 -39
  19. package/ios/Core/ApexNavigation.mm +1 -4
  20. package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
  21. package/ios/Layouts/Overlay/ANNOverlayCoordinator.mm +1 -1
  22. package/ios/Layouts/Root/ANNRootCoordinator.mm +1 -1
  23. package/ios/Layouts/Screen/ANNComponentViewController.h +9 -15
  24. package/ios/Layouts/Screen/ANNComponentViewController.mm +6 -42
  25. package/ios/Layouts/Screen/ANNScreenPlace.h +12 -0
  26. package/ios/Layouts/Screen/ANNScreenPlace.mm +46 -16
  27. package/ios/Layouts/SideMenu/ANNSideMenuTypes.h +8 -0
  28. package/ios/Layouts/SideMenu/ANNSideMenuViewController.mm +83 -14
  29. package/ios/Layouts/Stack/ANNStackController.mm +5 -2
  30. package/ios/Layouts/Stack/ANNStackQueue.h +2 -1
  31. package/ios/Layouts/Tabs/ANNBottomTabsController.h +3 -0
  32. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +222 -51
  33. package/ios/Options/ANNSideMenuOptions.h +2 -0
  34. package/ios/Options/ANNSideMenuOptions.mm +2 -0
  35. package/ios/React/ANNSafeAreaViewComponentView.mm +2 -2
  36. package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -1
  37. package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
  38. package/lib/typescript/interfaces/options/sideMenu.d.ts +16 -0
  39. package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/interfaces/options/bottomTabs.ts +2 -1
  42. package/src/interfaces/options/sideMenu.ts +17 -0
  43. package/ios/Layouts/Tabs/ANNTabsPreloadWindow.h +0 -27
  44. package/ios/Layouts/Tabs/ANNTabsPreloadWindow.mm +0 -53
@@ -47,10 +47,10 @@ static NSUInteger ANNRenderWaitCount;
47
47
  // The content nodes still waited, and every node the wait set its ready handler on.
48
48
  NSHashTable<UIViewController *> *_waited;
49
49
  NSHashTable<UIViewController *> *_listened;
50
- // The waited screens of the "root mounted" kind (a subset of `_waited`), and whether their limit passed in this
51
- // start (a retake waits for them no more then).
52
- NSHashTable<UIViewController *> *_mountWaited;
53
- BOOL _mountLimitPassed;
50
+ // The waited screens of the "unseen" kind (a subset of `_waited`), and whether their limit passed in this start (a
51
+ // retake waits for them no more then).
52
+ NSHashTable<UIViewController *> *_unseenWaited;
53
+ BOOL _unseenLimitPassed;
54
54
  ANNBarComponentsWait *_barWait;
55
55
  // What the last take of the running phase took: its screens (weakly) and, in the content phase, what the take read
56
56
  // of each to decide what it waits for (`takeStateOfScreens:`). A retake that would take the same does nothing.
@@ -75,7 +75,7 @@ static NSUInteger ANNRenderWaitCount;
75
75
  stringWithFormat:@"ApexNavigation.renderWait.%lu", (unsigned long)++ANNRenderWaitCount];
76
76
  _waited = [NSHashTable weakObjectsHashTable];
77
77
  _listened = [NSHashTable weakObjectsHashTable];
78
- _mountWaited = [NSHashTable weakObjectsHashTable];
78
+ _unseenWaited = [NSHashTable weakObjectsHashTable];
79
79
  }
80
80
  return self;
81
81
  }
@@ -89,7 +89,7 @@ static NSUInteger ANNRenderWaitCount;
89
89
  }
90
90
  _isWaiting = YES;
91
91
  _isInBarPhase = NO;
92
- _mountLimitPassed = NO;
92
+ _unseenLimitPassed = NO;
93
93
  _startTime = CACurrentMediaTime();
94
94
  NSUInteger number = ++_number;
95
95
  _starting = YES;
@@ -108,10 +108,10 @@ static NSUInteger ANNRenderWaitCount;
108
108
  [weakSelf limitOfNumber:number];
109
109
  });
110
110
  }
111
- if (_mountLimit > 0) {
112
- int64_t delay = (int64_t)(_mountLimit * NSEC_PER_SEC);
111
+ if (_unseenLimit > 0) {
112
+ int64_t delay = (int64_t)(_unseenLimit * NSEC_PER_SEC);
113
113
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay), dispatch_get_main_queue(), ^{
114
- [weakSelf mountLimitOfNumber:number];
114
+ [weakSelf unseenLimitOfNumber:number];
115
115
  });
116
116
  }
117
117
  if (_unknownContentRule) {
@@ -162,8 +162,8 @@ static NSUInteger ANNRenderWaitCount;
162
162
  case ANNRenderWaitContentNone:
163
163
  break;
164
164
  case ANNRenderWaitContentEachScreen:
165
- // Each screen by the readiness it needs: its content, or the first mount of its root (an unselected
166
- // tab's screen). A screen with no content to wait for (a native screen) is ready.
165
+ // Each screen by its content; an unselected tab's to the unseen limit. A screen with no content to wait
166
+ // for (a native screen) is ready.
167
167
  for (UIViewController *node in screens) {
168
168
  if (![node isKindOfClass:ANNComponentViewController.class]) {
169
169
  continue;
@@ -177,25 +177,11 @@ static NSUInteger ANNRenderWaitCount;
177
177
  [self listenTo:screen number:number];
178
178
  continue;
179
179
  }
180
- if (_mountLimitPassed) {
180
+ if (_unseenLimitPassed) {
181
181
  continue;
182
182
  }
183
- [_mountWaited addObject:screen];
184
- // A slot screen's readiness is its mount: its ready handler serves.
185
- if (![content respondsToSelector:@selector(whenMounted:)]) {
186
- [self listenTo:screen number:number];
187
- continue;
188
- }
189
- if (content.hasMounted) {
190
- [_mountWaited removeObject:screen];
191
- continue;
192
- }
193
- [_waited addObject:screen];
194
- __weak ANNRenderWait *weakSelf = self;
195
- __weak UIViewController *weakScreen = screen;
196
- [content whenMounted:^{
197
- [weakSelf node:weakScreen readyOfNumber:number];
198
- }];
183
+ [_unseenWaited addObject:screen];
184
+ [self listenTo:screen number:number];
199
185
  }
200
186
  break;
201
187
  case ANNRenderWaitContentNode:
@@ -271,19 +257,19 @@ static NSUInteger ANNRenderWaitCount;
271
257
  [self phaseOfNumber:number endedMarkingReady:NO];
272
258
  }
273
259
 
274
- // The screens of the "root mounted" kind are waited no longer: nobody sees them, nothing of theirs is held.
275
- - (void)mountLimitOfNumber:(NSUInteger)number {
260
+ // The screens of the "unseen" kind are waited no longer: nobody sees them, nothing of theirs is held.
261
+ - (void)unseenLimitOfNumber:(NSUInteger)number {
276
262
  if (![self runs:number]) {
277
263
  return;
278
264
  }
279
- _mountLimitPassed = YES;
280
- if (_mountWaited.count == 0) {
265
+ _unseenLimitPassed = YES;
266
+ if (_unseenWaited.count == 0) {
281
267
  return;
282
268
  }
283
- for (UIViewController *node in _mountWaited.allObjects) {
269
+ for (UIViewController *node in _unseenWaited.allObjects) {
284
270
  [_waited removeObject:node];
285
271
  }
286
- [_mountWaited removeAllObjects];
272
+ [_unseenWaited removeAllObjects];
287
273
  if (_waited.count == 0 && !_barWait) {
288
274
  [self phaseOfNumber:number endedMarkingReady:NO];
289
275
  }
@@ -464,7 +450,7 @@ static NSUInteger ANNRenderWaitCount;
464
450
  }
465
451
  [_listened removeAllObjects];
466
452
  [_waited removeAllObjects];
467
- [_mountWaited removeAllObjects];
453
+ [_unseenWaited removeAllObjects];
468
454
  }
469
455
 
470
456
  #pragma mark - Helpers
@@ -476,15 +462,14 @@ static NSUInteger ANNRenderWaitCount;
476
462
  }
477
463
 
478
464
  // What the content phase's take reads of each screen to decide what it waits for: the readiness it needs, whether it
479
- // has its content yet, whether that content is ready, and whether its root mounted.
465
+ // has its content yet, and whether that content is ready.
480
466
  - (NSArray<NSNumber *> *)takeStateOfScreens:(NSArray<UIViewController *> *)screens {
481
467
  NSMutableArray<NSNumber *> *states = [NSMutableArray arrayWithCapacity:screens.count];
482
468
  for (UIViewController *node in screens) {
483
- NSUInteger state = (NSUInteger)ANNWaitedReadinessOf(node) << 3;
469
+ NSUInteger state = (NSUInteger)ANNWaitedReadinessOf(node) << 2;
484
470
  if ([node isKindOfClass:ANNComponentViewController.class]) {
485
471
  UIView<ANNComponentProtocol> *content = ((ANNComponentViewController *)node).reactView;
486
- BOOL mounted = [content respondsToSelector:@selector(hasMounted)] && content.hasMounted;
487
- state |= (content ? 1 : 0) | (content.isReady ? 2 : 0) | (mounted ? 4 : 0);
472
+ state |= (content ? 1 : 0) | (content.isReady ? 2 : 0);
488
473
  }
489
474
  [states addObject:@(state)];
490
475
  }
@@ -9,7 +9,6 @@
9
9
  #import "ANNReloadCoordinator.h"
10
10
  #import "ANNSlotHost.h"
11
11
  #import "ANNSplashViewController.h"
12
- #import "ANNTabsPreloadWindow.h"
13
12
  #import "ANNUtils.h"
14
13
  #import <React/RCTLog.h>
15
14
  #import <ReactCommon/RCTHost.h>
@@ -149,9 +148,7 @@ static UIWindowScene *ANNConnectedWindowScene(void) {
149
148
  ANNMainWindow.windowScene = windowScene;
150
149
  ANNMainWindow.frame = windowScene.coordinateSpace.bounds;
151
150
  [ANNMainWindow makeKeyAndVisible];
152
- // The tabs' preload windows, the overlay windows and the slot host's window made before the scene go where the
153
- // main window is.
154
- [ANNTabsPreloadWindow mainWindowSceneDidConnect:windowScene];
151
+ // The overlay windows and the slot host's window made before the scene go where the main window is.
155
152
  [ANNOverlayCoordinator mainWindowSceneDidConnect:windowScene];
156
153
  [ANNSlotHost mainWindowSceneDidConnect:windowScene];
157
154
  // A direction the app set before the window had a scene takes the scene now.
@@ -233,7 +233,7 @@ static void ANNCollectIds(UIViewController *node, NSMutableSet<NSString *> *ids)
233
233
  limit:contentFirst ? 0 : ANNModalRenderLimit
234
234
  bar:contentFirst ? ANNRenderWaitBarAfterContent : ANNRenderWaitBarWithContent
235
235
  barLimits:contentFirst ? ownPhase : withContent];
236
- wait.mountLimit = ANNModalRenderLimit;
236
+ wait.unseenLimit = ANNModalRenderLimit;
237
237
  wait.protect = ^(dispatch_block_t step) {
238
238
  [ANNModalCoordinator protect:weakModal step:step];
239
239
  };
@@ -179,7 +179,7 @@ static NSMutableArray<ANNOverlay *> *ANNOverlays;
179
179
  limit:ANNOverlayRenderLimit
180
180
  bar:ANNRenderWaitBarWithContent
181
181
  barLimits:withContent];
182
- wait.mountLimit = ANNOverlayRenderLimit;
182
+ wait.unseenLimit = ANNOverlayRenderLimit;
183
183
  wait.protect = ^(dispatch_block_t step) {
184
184
  [ANNOverlayCoordinator protectWaitOf:weakOverlay step:step];
185
185
  };
@@ -300,7 +300,7 @@ static __weak ANNRootShow *ANNAnimatingShow;
300
300
  barLimits:contentFirst ? ownPhase : withContent];
301
301
  // A screen the user won't see at once (an unselected tab's) never holds the root longer than the limit, also with
302
302
  // `waitForRender: true`.
303
- wait.mountLimit = ANNRootRenderLimit;
303
+ wait.unseenLimit = ANNRootRenderLimit;
304
304
  wait.protect = ^(dispatch_block_t step) {
305
305
  [ANNRootCoordinator protect:weakShow step:step];
306
306
  };
@@ -44,27 +44,21 @@ NS_ASSUME_NONNULL_BEGIN
44
44
  - (void)render;
45
45
 
46
46
  /// Where the screen will show while it waits for its content out of it (parked by a stack's or `showInColumn`'s wait,
47
- /// `ANNParking`): it lays out with that place's safe area and bars (`ANNScreenPlace`), as it will once it shows there.
48
- /// Asked at each layout, and kept past the parking until UIKit gives the screen a safe area of its own in a parent
49
- /// (the push lays it out once before that, with none), or as it appears. Nil: its view's own. Set by the one that
50
- /// parks the screen.
47
+ /// or in a tab rendered before it shows, `ANNParking`): it lays out with that place's safe area and bars
48
+ /// (`ANNScreenPlace`), as it will once it shows there. Asked at each layout, and kept past the parking until UIKit
49
+ /// gives the screen a safe area of its own in a parent (the push lays it out once before that, with none), or as it
50
+ /// appears. Nil: its view's own. Set by the one that parks the screen.
51
51
  @property(nonatomic, copy, nullable) ANNScreenPlace *_Nullable (^placeWhileParked)(void);
52
52
 
53
+ /// The place is kept until the screen appears, not only until UIKit gives it a safe area: a tab's screen rendered
54
+ /// before the tab shows gets a part of it first (the bar without the status bar) as UIKit lays the tab out. Set with
55
+ /// `placeWhileParked`, and goes with it.
56
+ @property(nonatomic) BOOL placeKeptUntilAppearance;
57
+
53
58
  /// Applies the "under bars" rule to the root frame again, then gives the modules the insets, the padding and the
54
59
  /// regions of the screen. Nothing before the view loaded; the safe area only in a window.
55
60
  - (void)updateRootLayout;
56
61
 
57
- /// The content view (only the core's React view, never the screen's own view) mounts unseen in `host` (the tabs'
58
- /// preload window) at the frame it has, while the screen's tab isn't shown. NO when there is nothing to lend: no React
59
- /// view of its own, in a window already, lent already, or the screen went.
60
- - (BOOL)lendContentTo:(UIView *)host;
61
-
62
- /// The lent content comes back into the screen's view at its root frame; also as the screen first appears.
63
- - (void)takeContentBack;
64
-
65
- /// The content is lent (`lendContentTo:`): `updateRootLayout` leaves its frame alone meanwhile.
66
- @property(nonatomic, readonly) BOOL contentIsLent;
67
-
68
62
  /// The transition that removes the screen starts (before its destroy): with `blurOnUnmount` in its resolved options,
69
63
  /// the editing inside its view ends and the keyboard goes (`blurOnUnmount`).
70
64
  - (void)screenWillLeave;
@@ -19,10 +19,7 @@
19
19
  #import "ANNTopBarPresenter.h"
20
20
  #import "ANNUtils.h"
21
21
 
22
- @implementation ANNComponentViewController {
23
- // Where the content is lent while it mounts unseen (the tabs' preload window).
24
- __weak UIView *_contentLentTo;
25
- }
22
+ @implementation ANNComponentViewController
26
23
 
27
24
  - (instancetype)initWithLayoutInfo:(ANNLayoutInfo *)layoutInfo
28
25
  creator:(id<ANNComponentViewCreator>)creator
@@ -146,8 +143,6 @@
146
143
  // command's order: the command reports instead.
147
144
  - (void)viewWillAppear:(BOOL)animated {
148
145
  [super viewWillAppear:animated];
149
- // A tab shows for the first time while its content still mounts in the preload window.
150
- [self takeContentBack];
151
146
  // A preview screen that UIKit's menu takes leaves its unseen place first, so that the menu reports it.
152
147
  [ANNPreviewPush screenWillAppear:self];
153
148
  if (![ANNAppearanceDispatcher isAppearanceDriven:self]) {
@@ -166,6 +161,7 @@
166
161
  // A parked screen whose place gave it no safe area at all lays out with its view's own from now on.
167
162
  if (self.placeWhileParked && ![ANNParking isUnseen:self]) {
168
163
  self.placeWhileParked = nil;
164
+ self.placeKeptUntilAppearance = NO;
169
165
  [self updateRootLayout];
170
166
  }
171
167
  // Once JavaScript heard the appearance (a driven one ends later, with its command).
@@ -377,7 +373,8 @@
377
373
  if (!place) {
378
374
  return nil;
379
375
  }
380
- if (![ANNParking isUnseen:self] && self.parentViewController &&
376
+ if (!self.placeKeptUntilAppearance && ![ANNParking isUnseen:self] &&
377
+ self.parentViewController &&
381
378
  !UIEdgeInsetsEqualToEdgeInsets(self.view.safeAreaInsets, UIEdgeInsetsZero)) {
382
379
  self.placeWhileParked = nil;
383
380
  return nil;
@@ -402,7 +399,7 @@
402
399
  }
403
400
  ANNNavigationOptions *options = [self resolveOptionsWithDefault];
404
401
  UIView<ANNComponentProtocol> *content = _reactView;
405
- if (content && !self.contentIsLent) {
402
+ if (content) {
406
403
  CGRect frame = [self rootFrameForOptions:options];
407
404
  if (!CGRectEqualToRect(content.frame, frame)) {
408
405
  content.frame = frame;
@@ -553,37 +550,6 @@
553
550
  });
554
551
  }
555
552
 
556
- #pragma mark - The preload window
557
-
558
- - (BOOL)contentIsLent {
559
- return _contentLentTo != nil;
560
- }
561
-
562
- - (BOOL)lendContentTo:(UIView *)host {
563
- UIView<ANNComponentProtocol> *content = _reactView;
564
- if (!content || _contentLentTo || content.window || self.nodeHelper.destroyed ||
565
- ![content isKindOfClass:ANNReactView.class]) {
566
- return NO;
567
- }
568
- _contentLentTo = host;
569
- [host addSubview:content];
570
- return YES;
571
- }
572
-
573
- - (void)takeContentBack {
574
- if (!_contentLentTo) {
575
- return;
576
- }
577
- _contentLentTo = nil;
578
- UIView<ANNComponentProtocol> *content = _reactView;
579
- if (!content || self.nodeHelper.destroyed) {
580
- [content removeFromSuperview];
581
- return;
582
- }
583
- [self.view insertSubview:content atIndex:0];
584
- [self updateRootLayout];
585
- }
586
-
587
553
  #pragma mark - Leaving
588
554
 
589
555
  - (void)screenWillLeave {
@@ -674,9 +640,7 @@
674
640
  [topBar releaseLists];
675
641
  [topBar stopComponents];
676
642
  [self destroyReactView];
677
- // The stop holds the hosting view until the surface stopped: the screen lets go of its content view now, wherever
678
- // it is (a preload window too).
679
- _contentLentTo = nil;
643
+ // The stop holds the hosting view until the surface stopped: the screen lets go of its content view now.
680
644
  [_reactView removeFromSuperview];
681
645
  // The data is kept by id: a node that holds the id now (a new JavaScript reused it while this screen waited for
682
646
  // its destroy, as at a reload) keeps it.
@@ -27,6 +27,18 @@ NS_ASSUME_NONNULL_BEGIN
27
27
  inStack:(UINavigationController *)stack
28
28
  options:(ANNNavigationOptions *)options;
29
29
 
30
+ /// The same, measured in `host` instead of the stack's view: for a stack UIKit gives no safe area yet (a tab not shown,
31
+ /// parked in the tabs' view), `host` is a view with the safe area the stack gets as it shows (the shown tab's). Nil
32
+ /// when `host` isn't in a window.
33
+ + (nullable ANNScreenPlace *)placeOfEntry:(UIViewController *)entry
34
+ inStack:(UINavigationController *)stack
35
+ measuredIn:(UIView *)host
36
+ options:(ANNNavigationOptions *)options;
37
+
38
+ /// The place of a screen that is a tab of its own, with no stack: the safe area of `host` (the shown tab's view), no
39
+ /// top bar. Nil when `host` isn't in a window.
40
+ + (nullable ANNScreenPlace *)placeOfScreen:(UIViewController *)screen measuredIn:(UIView *)host;
41
+
30
42
  /// The safe area of the entry's view in the place.
31
43
  @property(nonatomic, readonly) UIEdgeInsets safeArea;
32
44
 
@@ -49,17 +49,16 @@ static void ANNCopyBarItem(UINavigationItem *from, UINavigationItem *to) {
49
49
  }
50
50
 
51
51
  // Everything the measure reads: the same answer while none of it changed.
52
- static NSString *ANNPlaceSignature(UINavigationItem *item, UINavigationController *stack, BOOL showsTopBar,
53
- BOOL showsToolbar) {
54
- UIView *stackView = stack.view;
52
+ static NSString *ANNPlaceSignature(UINavigationItem *item, UINavigationController *stack,
53
+ UIView *host, BOOL showsTopBar, BOOL showsToolbar) {
55
54
  NSMutableString *signature = [NSMutableString
56
55
  stringWithFormat:@"%@|%@|%ld|%p|%p|%p|%p|%p|%d|%d|%d|%d|%@|%@", item.title, item.prompt,
57
56
  (long)item.largeTitleDisplayMode, item.standardAppearance,
58
57
  item.scrollEdgeAppearance, item.compactAppearance,
59
58
  item.compactScrollEdgeAppearance, item.searchController,
60
59
  item.hidesSearchBarWhenScrolling, stack.navigationBar.prefersLargeTitles,
61
- showsTopBar, showsToolbar, NSStringFromCGSize(stackView.bounds.size),
62
- NSStringFromUIEdgeInsets(stackView.safeAreaInsets)];
60
+ showsTopBar, showsToolbar, NSStringFromCGSize(host.bounds.size),
61
+ NSStringFromUIEdgeInsets(host.safeAreaInsets)];
63
62
  [signature appendFormat:@"|%p|%ld", stack,
64
63
  (long)stack.topViewController.navigationItem.largeTitleDisplayMode];
65
64
  if (@available(iOS 16.0, *)) {
@@ -91,9 +90,19 @@ static NSString *ANNPlaceSignature(UINavigationItem *item, UINavigationControlle
91
90
  + (ANNScreenPlace *)placeOfEntry:(UIViewController *)entry
92
91
  inStack:(UINavigationController *)stack
93
92
  options:(ANNNavigationOptions *)options {
94
- ANNAssertMainQueue();
95
93
  UIView *stackView = stack.viewIfLoaded;
96
- UIWindow *window = stackView.window;
94
+ if (!stackView) {
95
+ return nil;
96
+ }
97
+ return [self placeOfEntry:entry inStack:stack measuredIn:stackView options:options];
98
+ }
99
+
100
+ + (ANNScreenPlace *)placeOfEntry:(UIViewController *)entry
101
+ inStack:(UINavigationController *)stack
102
+ measuredIn:(UIView *)host
103
+ options:(ANNNavigationOptions *)options {
104
+ ANNAssertMainQueue();
105
+ UIWindow *window = host.window;
97
106
  if (!window) {
98
107
  return nil;
99
108
  }
@@ -102,19 +111,20 @@ static NSString *ANNPlaceSignature(UINavigationItem *item, UINavigationControlle
102
111
  BOOL showsTabBar = !entry.hidesBottomBarWhenPushed &&
103
112
  [[ANNBottomTabsController tabsOf:entry] showsTabBarForScreen:entry];
104
113
  NSString *signature =
105
- ANNPlaceSignature(entry.navigationItem, stack, showsTopBar, showsToolbar);
114
+ ANNPlaceSignature(entry.navigationItem, stack, host, showsTopBar, showsToolbar);
106
115
  ANNScreenPlace *kept = objc_getAssociatedObject(entry, ANNScreenPlaceKey);
107
116
  if (kept && kept.showsTabBar == showsTabBar && [kept->_signature isEqualToString:signature]) {
108
117
  return kept;
109
118
  }
110
119
  UIEdgeInsets safeArea = [self measureEntry:entry
111
120
  inStack:stack
121
+ host:host
112
122
  showsTopBar:showsTopBar
113
123
  showsToolbar:showsToolbar];
114
- // The measure stands in the stack's view, whose safe area has the tab bar shown now: an entry that hides it gets
115
- // the window's bottom there.
124
+ // The measure stands in the host, whose safe area has the tab bar shown now: an entry that hides it gets the
125
+ // window's bottom there.
116
126
  if (!showsTabBar) {
117
- CGFloat tabBar = stackView.safeAreaInsets.bottom - window.safeAreaInsets.bottom;
127
+ CGFloat tabBar = host.safeAreaInsets.bottom - window.safeAreaInsets.bottom;
118
128
  if (tabBar > 0) {
119
129
  safeArea.bottom = MAX(safeArea.bottom - tabBar, 0);
120
130
  }
@@ -127,10 +137,31 @@ static NSString *ANNPlaceSignature(UINavigationItem *item, UINavigationControlle
127
137
  return place;
128
138
  }
129
139
 
130
- // The probe lays out at the stack's size in the stack's view, unseen and for this moment only, and leaves no search
131
- // field behind.
140
+ + (ANNScreenPlace *)placeOfScreen:(UIViewController *)screen measuredIn:(UIView *)host {
141
+ ANNAssertMainQueue();
142
+ UIWindow *window = host.window;
143
+ if (!window) {
144
+ return nil;
145
+ }
146
+ BOOL showsTabBar = [[ANNBottomTabsController tabsOf:screen] showsTabBarForScreen:screen];
147
+ UIEdgeInsets safeArea = host.safeAreaInsets;
148
+ if (!showsTabBar) {
149
+ CGFloat tabBar = safeArea.bottom - window.safeAreaInsets.bottom;
150
+ if (tabBar > 0) {
151
+ safeArea.bottom = MAX(safeArea.bottom - tabBar, 0);
152
+ }
153
+ }
154
+ return [[ANNScreenPlace alloc] initWithSafeArea:safeArea
155
+ showsTopBar:NO
156
+ showsTabBar:showsTabBar
157
+ signature:@""];
158
+ }
159
+
160
+ // The probe lays out at the host's size in the host, unseen and for this moment only, and leaves no search field
161
+ // behind.
132
162
  + (UIEdgeInsets)measureEntry:(UIViewController *)entry
133
163
  inStack:(UINavigationController *)stack
164
+ host:(UIView *)host
134
165
  showsTopBar:(BOOL)showsTopBar
135
166
  showsToolbar:(BOOL)showsToolbar {
136
167
  UINavigationController *probe = ANNPlaceProbeOf(stack);
@@ -152,7 +183,6 @@ static NSString *ANNPlaceSignature(UINavigationItem *item, UINavigationControlle
152
183
  action:nil] ]
153
184
  : nil;
154
185
  __block UIEdgeInsets safeArea = UIEdgeInsetsZero;
155
- UIView *stackView = stack.view;
156
186
  UIView *probeView = probe.view;
157
187
  [UIView performWithoutAnimation:^{
158
188
  [probe setNavigationBarHidden:!showsTopBar animated:NO];
@@ -160,8 +190,8 @@ static NSString *ANNPlaceSignature(UINavigationItem *item, UINavigationControlle
160
190
  probeView.alpha = 0;
161
191
  probeView.userInteractionEnabled = NO;
162
192
  probeView.accessibilityElementsHidden = YES;
163
- probeView.frame = stackView.bounds;
164
- [stackView insertSubview:probeView atIndex:0];
193
+ probeView.frame = host.bounds;
194
+ [host insertSubview:probeView atIndex:0];
165
195
  [probeView layoutIfNeeded];
166
196
  [top.view layoutIfNeeded];
167
197
  safeArea = top.view.safeAreaInsets;
@@ -12,6 +12,14 @@ typedef NS_ENUM(NSInteger, ANNSideMenuOpenMode) {
12
12
  ANNSideMenuOpenModeAboveContent,
13
13
  };
14
14
 
15
+ /** `sideMenu.menusAttachMode`: when the screens of the menus are made, with the side menu, after its first
16
+ * appearance, or each as it first starts to open. */
17
+ typedef NS_ENUM(NSInteger, ANNSideMenuAttachMode) {
18
+ ANNSideMenuAttachModeTogether,
19
+ ANNSideMenuAttachModeAfterCenter,
20
+ ANNSideMenuAttachModeOnOpen,
21
+ };
22
+
15
23
  /** `sideMenu.openGestureMode`: a pan from anywhere on the center, or from its edges only. */
16
24
  typedef NS_ENUM(NSInteger, ANNSideMenuOpenGestureMode) {
17
25
  ANNSideMenuOpenGestureModeEntireScreen,
@@ -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 {