@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.
- package/CHANGELOG.md +13 -0
- package/android/src/main/java/dev/apexnative/navigation/options/SideMenuOptions.kt +6 -1
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +36 -5
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -0
- package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +35 -3
- package/android/src/main/java/dev/apexnative/navigation/views/sidemenu/SideMenuView.kt +12 -3
- package/android/src/main/java/dev/apexnative/navigation/views/stack/ScreenEdge.kt +34 -4
- package/android/src/main/java/dev/apexnative/navigation/views/stack/StackLayout.kt +6 -3
- package/docs-md/containers/side-menu.md +15 -0
- package/docs-md/containers/tabs.md +1 -1
- package/docs-md/index.json +4 -0
- package/ios/CLAUDE.md +4 -1
- package/ios/CORE.md +2 -2
- package/ios/Core/ANNLayoutProtocol.h +5 -5
- package/ios/Core/ANNParking.h +5 -4
- package/ios/Core/ANNReloadCoordinator.mm +1 -4
- package/ios/Core/ANNRenderWait.h +5 -5
- package/ios/Core/ANNRenderWait.mm +24 -39
- package/ios/Core/ApexNavigation.mm +1 -4
- package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
- package/ios/Layouts/Overlay/ANNOverlayCoordinator.mm +1 -1
- package/ios/Layouts/Root/ANNRootCoordinator.mm +1 -1
- package/ios/Layouts/Screen/ANNComponentViewController.h +9 -15
- package/ios/Layouts/Screen/ANNComponentViewController.mm +6 -42
- package/ios/Layouts/Screen/ANNScreenPlace.h +12 -0
- package/ios/Layouts/Screen/ANNScreenPlace.mm +46 -16
- package/ios/Layouts/SideMenu/ANNSideMenuTypes.h +8 -0
- package/ios/Layouts/SideMenu/ANNSideMenuViewController.mm +83 -14
- package/ios/Layouts/Stack/ANNStackController.mm +5 -2
- package/ios/Layouts/Stack/ANNStackQueue.h +2 -1
- package/ios/Layouts/Tabs/ANNBottomTabsController.h +3 -0
- package/ios/Layouts/Tabs/ANNBottomTabsController.mm +222 -51
- package/ios/Options/ANNSideMenuOptions.h +2 -0
- package/ios/Options/ANNSideMenuOptions.mm +2 -0
- package/ios/React/ANNSafeAreaViewComponentView.mm +2 -2
- package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -1
- package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
- package/lib/typescript/interfaces/options/sideMenu.d.ts +16 -0
- package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/interfaces/options/bottomTabs.ts +2 -1
- package/src/interfaces/options/sideMenu.ts +17 -0
- package/ios/Layouts/Tabs/ANNTabsPreloadWindow.h +0 -27
- 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 "
|
|
51
|
-
//
|
|
52
|
-
NSHashTable<UIViewController *> *
|
|
53
|
-
BOOL
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
112
|
-
int64_t delay = (int64_t)(
|
|
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
|
|
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
|
|
166
|
-
//
|
|
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 (
|
|
180
|
+
if (_unseenLimitPassed) {
|
|
181
181
|
continue;
|
|
182
182
|
}
|
|
183
|
-
[
|
|
184
|
-
|
|
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 "
|
|
275
|
-
- (void)
|
|
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
|
-
|
|
280
|
-
if (
|
|
265
|
+
_unseenLimitPassed = YES;
|
|
266
|
+
if (_unseenWaited.count == 0) {
|
|
281
267
|
return;
|
|
282
268
|
}
|
|
283
|
-
for (UIViewController *node in
|
|
269
|
+
for (UIViewController *node in _unseenWaited.allObjects) {
|
|
284
270
|
[_waited removeObject:node];
|
|
285
271
|
}
|
|
286
|
-
[
|
|
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
|
-
[
|
|
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
|
|
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) <<
|
|
469
|
+
NSUInteger state = (NSUInteger)ANNWaitedReadinessOf(node) << 2;
|
|
484
470
|
if ([node isKindOfClass:ANNComponentViewController.class]) {
|
|
485
471
|
UIView<ANNComponentProtocol> *content = ((ANNComponentViewController *)node).reactView;
|
|
486
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
48
|
-
/// Asked at each layout, and kept past the parking until UIKit
|
|
49
|
-
/// (the push lays it out once before that, with none), or as it
|
|
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] &&
|
|
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
|
|
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
|
|
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,
|
|
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(
|
|
62
|
-
NSStringFromUIEdgeInsets(
|
|
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
|
-
|
|
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
|
|
115
|
-
//
|
|
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 =
|
|
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
|
-
|
|
131
|
-
|
|
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 =
|
|
164
|
-
[
|
|
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
|
|
185
|
-
_rightContainer = [self
|
|
186
|
-
_centerContainer = [self
|
|
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
|
-
//
|
|
204
|
-
- (UIView *)
|
|
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 {
|