@apexnative/navigation 1.0.0-alpha.3 → 1.0.0-alpha.4

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 (193) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/android/CLAUDE.md +17 -7
  3. package/android/CORE.md +8 -3
  4. package/android/RECIPES.md +9 -3
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
  6. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
  7. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
  8. package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
  9. package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
  10. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
  11. package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
  12. package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
  13. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +2 -24
  14. package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
  15. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +27 -19
  16. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
  17. package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
  18. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +17 -7
  19. package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
  20. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
  22. package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
  23. package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
  24. package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
  25. package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
  26. package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +16 -6
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +5 -5
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +4 -3
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +6 -3
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +8 -8
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -1
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +58 -15
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +16 -7
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +2 -1
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +85 -29
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +2 -2
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +3 -3
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +10 -2
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +56 -21
  50. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
  51. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
  52. package/cli/doctor/checks/app.js +61 -13
  53. package/cli/migrate/native.js +18 -0
  54. package/cli/setup/source.js +2 -1
  55. package/cli/setup/steps/mainApplication.js +134 -19
  56. package/docs-md/architecture/transitions.md +6 -7
  57. package/docs-md/help/glossary.md +1 -1
  58. package/docs-md/help/troubleshooting.md +20 -3
  59. package/docs-md/index.json +4 -0
  60. package/docs-md/migration/from-react-native-navigation.md +8 -2
  61. package/docs-md/screen-ui/back-and-gestures.md +2 -2
  62. package/docs-md/start/installation.md +3 -1
  63. package/ios/CLAUDE.md +17 -5
  64. package/ios/CORE.md +13 -8
  65. package/ios/Contract/ANNCommandAnswer.mm +5 -20
  66. package/ios/Contract/ANNCommandContract.h +15 -0
  67. package/ios/Contract/ANNCommandContract.mm +78 -0
  68. package/ios/Core/ANNAppearanceDispatcher.mm +1 -1
  69. package/ios/Core/ANNErrorHandler.h +1 -1
  70. package/ios/Core/ANNErrorHandler.mm +3 -1
  71. package/ios/Core/ANNEventEmitter.h +1 -1
  72. package/ios/Core/ANNEventEmitter.mm +2 -2
  73. package/ios/Core/ANNJavaScriptCalls.h +34 -0
  74. package/ios/Core/ANNJavaScriptCalls.mm +61 -0
  75. package/ios/Core/ANNNavigationState.h +7 -3
  76. package/ios/Core/ANNNavigationState.mm +10 -5
  77. package/ios/Core/ANNNodeHelper.h +32 -12
  78. package/ios/Core/ANNNodeHelper.mm +100 -3
  79. package/ios/Core/ANNRenderWait.h +15 -1
  80. package/ios/Core/ANNRenderWait.mm +119 -17
  81. package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
  82. package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
  83. package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
  84. package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
  85. package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
  86. package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
  87. package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
  88. package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
  89. package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
  90. package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
  91. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
  92. package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
  93. package/ios/Layouts/Stack/ANNStackController.mm +26 -3
  94. package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
  95. package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
  96. package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
  97. package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
  98. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
  99. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
  100. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
  101. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
  102. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
  103. package/ios/Options/ANNOptionsCache.h +7 -6
  104. package/ios/Options/ANNSearchBarOptions.h +4 -3
  105. package/ios/Options/ANNUserChoice.h +57 -0
  106. package/ios/Options/ANNUserChoice.mm +44 -0
  107. package/ios/Options/Params/ANNImageSource.h +61 -0
  108. package/ios/Options/Params/ANNImageSource.mm +101 -0
  109. package/ios/Options/Params/ANNOptionImages.h +37 -0
  110. package/ios/Options/Params/ANNOptionImages.mm +284 -0
  111. package/ios/Options/Params/Image.h +11 -2
  112. package/ios/Options/Params/Image.mm +15 -2
  113. package/ios/Options/Params/ImageParser.h +5 -5
  114. package/ios/Options/Params/ImageParser.mm +28 -46
  115. package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
  116. package/ios/RECIPES.md +11 -5
  117. package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
  118. package/ios/React/Slots/ANNSlotContainers.h +14 -6
  119. package/ios/React/Slots/ANNSlotContainers.mm +75 -2
  120. package/ios/TopBar/ANNBarButtons.h +9 -5
  121. package/ios/TopBar/ANNBarButtons.mm +211 -63
  122. package/ios/TopBar/ANNIconButtonImage.h +1 -0
  123. package/ios/TopBar/ANNIconButtonImage.mm +34 -1
  124. package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
  125. package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
  126. package/ios/TopBar/ANNSearchPresenter.mm +170 -19
  127. package/ios/TopBar/ANNTopBarPresenter.h +5 -0
  128. package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
  129. package/ios/TurboModules/ANNTurboModule.mm +39 -21
  130. package/lib/module/ARCHITECTURE.md +18 -18
  131. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  132. package/lib/module/api/navigation.js.map +1 -1
  133. package/lib/module/engine/commands.js +9 -7
  134. package/lib/module/engine/commands.js.map +1 -1
  135. package/lib/module/engine/layout.js +13 -9
  136. package/lib/module/engine/layout.js.map +1 -1
  137. package/lib/module/engine/options.js +44 -11
  138. package/lib/module/engine/options.js.map +1 -1
  139. package/lib/module/engine/places.js +44 -7
  140. package/lib/module/engine/places.js.map +1 -1
  141. package/lib/module/engine/props.js +30 -1
  142. package/lib/module/engine/props.js.map +1 -1
  143. package/lib/module/engine/rememberedOptions.js +1 -1
  144. package/lib/module/engine/rememberedOptions.js.map +1 -1
  145. package/lib/module/engine/screens.js +22 -12
  146. package/lib/module/engine/screens.js.map +1 -1
  147. package/lib/module/interfaces/contract/commands.json +101 -0
  148. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
  149. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  150. package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
  151. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  152. package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
  153. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  154. package/lib/typescript/api/navigation.d.ts +4 -1
  155. package/lib/typescript/api/navigation.d.ts.map +1 -1
  156. package/lib/typescript/engine/commands.d.ts.map +1 -1
  157. package/lib/typescript/engine/options.d.ts +5 -2
  158. package/lib/typescript/engine/options.d.ts.map +1 -1
  159. package/lib/typescript/engine/places.d.ts +5 -2
  160. package/lib/typescript/engine/places.d.ts.map +1 -1
  161. package/lib/typescript/engine/props.d.ts +8 -1
  162. package/lib/typescript/engine/props.d.ts.map +1 -1
  163. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  164. package/lib/typescript/engine/screens.d.ts.map +1 -1
  165. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  166. package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
  167. package/lib/typescript/interfaces/Events.d.ts +19 -10
  168. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  169. package/lib/typescript/interfaces/options/animations.d.ts +5 -2
  170. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  171. package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
  172. package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
  173. package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
  174. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  175. package/package.json +1 -1
  176. package/src/ARCHITECTURE.md +18 -18
  177. package/src/adapters/NativeANNTurboModule.ts +5 -2
  178. package/src/api/events/navigationEvents.ts +1 -1
  179. package/src/api/hooks/useNavigation.ts +1 -1
  180. package/src/api/navigation.ts +4 -1
  181. package/src/engine/commands.ts +19 -7
  182. package/src/engine/layout.ts +16 -9
  183. package/src/engine/options.ts +39 -11
  184. package/src/engine/places.ts +56 -7
  185. package/src/engine/props.ts +30 -1
  186. package/src/engine/rememberedOptions.ts +4 -1
  187. package/src/engine/screens.tsx +23 -13
  188. package/src/interfaces/ComponentEvents.ts +2 -2
  189. package/src/interfaces/Events.ts +19 -10
  190. package/src/interfaces/contract/commands.json +101 -0
  191. package/src/interfaces/options/animations.ts +5 -2
  192. package/src/interfaces/options/buttons.ts +13 -5
  193. package/src/interfaces/options/topBar.ts +22 -9
@@ -0,0 +1,175 @@
1
+ #import "ANNScreenPlace.h"
2
+ #import "ANNAssert.h"
3
+ #import "ANNBottomTabsController.h"
4
+ #import <objc/runtime.h>
5
+
6
+ static const void *ANNScreenPlaceKey = &ANNScreenPlaceKey;
7
+
8
+ // The measuring navigation controller of each stack: its first item stands for the stack's top, the second for the
9
+ // entry. Weak keys: it goes with its stack.
10
+ static NSMapTable<UINavigationController *, UINavigationController *> *ANNPlaceProbes(void) {
11
+ static NSMapTable<UINavigationController *, UINavigationController *> *probes;
12
+ if (!probes) {
13
+ probes = [NSMapTable weakToStrongObjectsMapTable];
14
+ }
15
+ return probes;
16
+ }
17
+
18
+ static UINavigationController *ANNPlaceProbeOf(UINavigationController *stack) {
19
+ UINavigationController *probe = [ANNPlaceProbes() objectForKey:stack];
20
+ if (!probe) {
21
+ probe = [[UINavigationController alloc] initWithRootViewController:[UIViewController new]];
22
+ [probe pushViewController:[UIViewController new] animated:NO];
23
+ [ANNPlaceProbes() setObject:probe forKey:stack];
24
+ }
25
+ return probe;
26
+ }
27
+
28
+ // What UIKit sizes the bar by, from the entry's item. The search field is a field of its own with the same placement:
29
+ // a search controller shows in one item only.
30
+ static void ANNCopyBarItem(UINavigationItem *from, UINavigationItem *to) {
31
+ to.title = from.title;
32
+ to.prompt = from.prompt;
33
+ to.largeTitleDisplayMode = from.largeTitleDisplayMode;
34
+ to.standardAppearance = from.standardAppearance;
35
+ to.scrollEdgeAppearance = from.scrollEdgeAppearance;
36
+ to.compactAppearance = from.compactAppearance;
37
+ to.compactScrollEdgeAppearance = from.compactScrollEdgeAppearance;
38
+ to.hidesSearchBarWhenScrolling = from.hidesSearchBarWhenScrolling;
39
+ to.searchController =
40
+ from.searchController ? [[UISearchController alloc] initWithSearchResultsController:nil] : nil;
41
+ if (@available(iOS 16.0, *)) {
42
+ to.style = from.style;
43
+ to.preferredSearchBarPlacement = from.preferredSearchBarPlacement;
44
+ }
45
+ if (@available(iOS 26.0, *)) {
46
+ to.attributedSubtitle = from.attributedSubtitle;
47
+ to.largeAttributedSubtitle = from.largeAttributedSubtitle;
48
+ }
49
+ }
50
+
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;
55
+ NSMutableString *signature = [NSMutableString
56
+ stringWithFormat:@"%@|%@|%ld|%p|%p|%p|%p|%p|%d|%d|%d|%d|%@|%@", item.title, item.prompt,
57
+ (long)item.largeTitleDisplayMode, item.standardAppearance,
58
+ item.scrollEdgeAppearance, item.compactAppearance,
59
+ item.compactScrollEdgeAppearance, item.searchController,
60
+ item.hidesSearchBarWhenScrolling, stack.navigationBar.prefersLargeTitles,
61
+ showsTopBar, showsToolbar, NSStringFromCGSize(stackView.bounds.size),
62
+ NSStringFromUIEdgeInsets(stackView.safeAreaInsets)];
63
+ [signature appendFormat:@"|%p|%ld", stack,
64
+ (long)stack.topViewController.navigationItem.largeTitleDisplayMode];
65
+ if (@available(iOS 16.0, *)) {
66
+ [signature appendFormat:@"|%ld|%ld", (long)item.style, (long)item.preferredSearchBarPlacement];
67
+ }
68
+ if (@available(iOS 26.0, *)) {
69
+ [signature appendFormat:@"|%@|%@", item.attributedSubtitle, item.largeAttributedSubtitle];
70
+ }
71
+ return signature;
72
+ }
73
+
74
+ @implementation ANNScreenPlace {
75
+ NSString *_signature;
76
+ }
77
+
78
+ - (instancetype)initWithSafeArea:(UIEdgeInsets)safeArea
79
+ showsTopBar:(BOOL)showsTopBar
80
+ showsTabBar:(BOOL)showsTabBar
81
+ signature:(NSString *)signature {
82
+ if (self = [super init]) {
83
+ _safeArea = safeArea;
84
+ _showsTopBar = showsTopBar;
85
+ _showsTabBar = showsTabBar;
86
+ _signature = [signature copy];
87
+ }
88
+ return self;
89
+ }
90
+
91
+ + (ANNScreenPlace *)placeOfEntry:(UIViewController *)entry
92
+ inStack:(UINavigationController *)stack
93
+ options:(ANNNavigationOptions *)options {
94
+ ANNAssertMainQueue();
95
+ UIView *stackView = stack.viewIfLoaded;
96
+ UIWindow *window = stackView.window;
97
+ if (!window) {
98
+ return nil;
99
+ }
100
+ BOOL showsTopBar = [options.topBar.visible withDefault:YES];
101
+ BOOL showsToolbar = options.toolbar.shouldShow;
102
+ BOOL showsTabBar = !entry.hidesBottomBarWhenPushed &&
103
+ [[ANNBottomTabsController tabsOf:entry] showsTabBarForScreen:entry];
104
+ NSString *signature =
105
+ ANNPlaceSignature(entry.navigationItem, stack, showsTopBar, showsToolbar);
106
+ ANNScreenPlace *kept = objc_getAssociatedObject(entry, ANNScreenPlaceKey);
107
+ if (kept && kept.showsTabBar == showsTabBar && [kept->_signature isEqualToString:signature]) {
108
+ return kept;
109
+ }
110
+ UIEdgeInsets safeArea = [self measureEntry:entry
111
+ inStack:stack
112
+ showsTopBar:showsTopBar
113
+ 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.
116
+ if (!showsTabBar) {
117
+ CGFloat tabBar = stackView.safeAreaInsets.bottom - window.safeAreaInsets.bottom;
118
+ if (tabBar > 0) {
119
+ safeArea.bottom = MAX(safeArea.bottom - tabBar, 0);
120
+ }
121
+ }
122
+ ANNScreenPlace *place = [[ANNScreenPlace alloc] initWithSafeArea:safeArea
123
+ showsTopBar:showsTopBar
124
+ showsTabBar:showsTabBar
125
+ signature:signature];
126
+ objc_setAssociatedObject(entry, ANNScreenPlaceKey, place, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
127
+ return place;
128
+ }
129
+
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.
132
+ + (UIEdgeInsets)measureEntry:(UIViewController *)entry
133
+ inStack:(UINavigationController *)stack
134
+ showsTopBar:(BOOL)showsTopBar
135
+ showsToolbar:(BOOL)showsToolbar {
136
+ UINavigationController *probe = ANNPlaceProbeOf(stack);
137
+ UIViewController *below = probe.viewControllers.firstObject;
138
+ UIViewController *top = probe.topViewController;
139
+ UINavigationBar *bar = probe.navigationBar;
140
+ UINavigationBar *stackBar = stack.navigationBar;
141
+ bar.prefersLargeTitles = stackBar.prefersLargeTitles;
142
+ bar.standardAppearance = stackBar.standardAppearance;
143
+ bar.scrollEdgeAppearance = stackBar.scrollEdgeAppearance;
144
+ bar.compactAppearance = stackBar.compactAppearance;
145
+ below.navigationItem.largeTitleDisplayMode =
146
+ stack.topViewController.navigationItem.largeTitleDisplayMode;
147
+ ANNCopyBarItem(entry.navigationItem, top.navigationItem);
148
+ top.toolbarItems =
149
+ showsToolbar ? @[ [[UIBarButtonItem alloc] initWithTitle:@" "
150
+ style:UIBarButtonItemStylePlain
151
+ target:nil
152
+ action:nil] ]
153
+ : nil;
154
+ __block UIEdgeInsets safeArea = UIEdgeInsetsZero;
155
+ UIView *stackView = stack.view;
156
+ UIView *probeView = probe.view;
157
+ [UIView performWithoutAnimation:^{
158
+ [probe setNavigationBarHidden:!showsTopBar animated:NO];
159
+ [probe setToolbarHidden:!showsToolbar animated:NO];
160
+ probeView.alpha = 0;
161
+ probeView.userInteractionEnabled = NO;
162
+ probeView.accessibilityElementsHidden = YES;
163
+ probeView.frame = stackView.bounds;
164
+ [stackView insertSubview:probeView atIndex:0];
165
+ [probeView layoutIfNeeded];
166
+ [top.view layoutIfNeeded];
167
+ safeArea = top.view.safeAreaInsets;
168
+ [probeView removeFromSuperview];
169
+ }];
170
+ top.navigationItem.searchController = nil;
171
+ top.toolbarItems = nil;
172
+ return safeArea;
173
+ }
174
+
175
+ @end
@@ -15,8 +15,9 @@ NS_ASSUME_NONNULL_BEGIN
15
15
  /// `showInColumn`: the push's checks of the layout (the options of the tree, what the core makes), then the screen is
16
16
  /// made and waits by the push's rules. Once it is ready, by the state then: the source in a split view not collapsed
17
17
  /// into its compact layout — the split view shows the screen in `column`, unless a later request of that column began
18
- /// meanwhile (the last one wins); else the source in a stack — a push over it; else a rejection. The answer is the
19
- /// shown screen's id; every rejection destroys the screen.
18
+ /// meanwhile (the last one wins; the superseded one answers when the last one does, with its own screen's id whatever
19
+ /// the last one's outcome, as on Android); else the source in a stack — a push over it; else a rejection. The answer
20
+ /// is the shown screen's id; every rejection destroys the screen.
20
21
  + (void)showInColumn:(NSString *)column
21
22
  layout:(ANNLayoutNode *)layout
22
23
  componentId:(NSString *)componentId
@@ -15,14 +15,19 @@
15
15
  #import "ANNRenderWait.h"
16
16
  #import "ANNShownScreens.h"
17
17
  #import "ANNStackCommands.h"
18
+ #import "ANNStackController.h"
18
19
  #import "ANNStackQueue.h"
19
20
  #import "ApexNavigation+Internal.h"
20
21
  #import "UIViewController+LayoutProtocol.h"
21
22
  #import <React/RCTLog.h>
22
23
 
23
- // The limits of the bar components of a push: title and background 1 s, buttons 0.5 s.
24
+ // The limits of the bar components of a push: title and background 1 s, buttons 0.5 s. With `waitForRender` unset they
25
+ // count from the wait's start, as a stack's push; with `true`, from the content's end.
24
26
  static const ANNBarWaitLimits ANNColumnBarLimits = {1, 0.5};
25
27
 
28
+ // How long the push's wait waits for the screen's first content when `waitForRender` is not set, as a stack's push.
29
+ static const NSTimeInterval ANNColumnRenderLimit = 0.5;
30
+
26
31
  // The split view of `node`: the node itself or the nearest of its layout parents that has columns.
27
32
  static UIViewController *ANNSplitViewOf(UIViewController *node) {
28
33
  for (UIViewController *parent = node; parent; parent = parent.layoutParent) {
@@ -45,17 +50,26 @@ static BOOL ANNIsOnScreen(UIViewController *node) {
45
50
 
46
51
  @class ANNColumnRequest;
47
52
 
53
+ // A request the newest of its column superseded: its answer and the id it resolves with, its own screen's (each
54
+ // `showInColumn` answers its own screen, as on Android).
55
+ @interface ANNSupersededRequest : NSObject
56
+ @property(nonatomic, strong) ANNCommandAnswer *answer;
57
+ @property(nonatomic, copy) NSString *value;
58
+ @end
59
+
60
+ @implementation ANNSupersededRequest
61
+ @end
62
+
48
63
  // "The last request of a column" (`showInColumn` step 2): one per split view and column. The newest request decides
49
- // what the requests it superseded answer: its id, or its rejection.
64
+ // when the requests it superseded answer; each of them resolves with its own screen's id whatever the newest one's
65
+ // outcome, as on Android.
50
66
  @interface ANNColumnRecord : NSObject
51
67
  @property(nonatomic, weak, nullable) UIViewController *splitView;
52
68
  @property(nonatomic, copy) NSString *column;
53
69
  // Weakly: what runs the newest request holds it (the waiting requests, the split view's call, the push).
54
70
  @property(nonatomic, weak, nullable) ANNColumnRequest *newest;
55
- @property(nonatomic, strong) NSMutableArray<ANNCommandAnswer *> *superseded;
71
+ @property(nonatomic, strong) NSMutableArray<ANNSupersededRequest *> *superseded;
56
72
  @property(nonatomic) BOOL ended;
57
- @property(nonatomic, copy, nullable) NSString *value;
58
- @property(nonatomic, copy, nullable) NSString *rejection;
59
73
  @end
60
74
 
61
75
  @implementation ANNColumnRecord
@@ -184,10 +198,11 @@ static NSMapTable<UIViewController *, ANNColumnRecordsOfSplitView *> *ANNColumnR
184
198
  return record;
185
199
  }
186
200
 
187
- // Step 1, the push's wait: `waitForRender` unset waits for the bar components only, `true` for the content as long as
188
- // it takes (with the rule for content nobody knows of), then the bar components; `false` for nothing; nothing either
189
- // while the source's place isn't seen. Read from the screen's own options over the defaults: it has no place yet. With
190
- // content, the screen waits unseen in the source's window, as a layer that waits to show.
201
+ // Step 1, the push's wait, as a stack's push: `waitForRender` unset waits for the content at most 0.5 s, `true` as long
202
+ // as it takes (with the rule for content nobody knows of), then the bar components; `false` for nothing; nothing either
203
+ // while the source's place isn't seen. The content's end waits for the calls JavaScript made by then, so that the
204
+ // options of the screen's first commit are applied before it shows. Read from the screen's own options over the
205
+ // defaults: it has no place yet. The screen waits unseen in the source's window, as a layer that waits to show.
191
206
  + (void)startWaitOf:(ANNColumnRequest *)request {
192
207
  UIViewController *screen = request.screen;
193
208
  Bool *waitForRender = [screen resolveOptionsWithDefault].animations.push.waitForRender;
@@ -198,20 +213,22 @@ static NSMapTable<UIViewController *, ANNColumnRecordsOfSplitView *> *ANNColumnR
198
213
  [self readyOnNextTurnOf:request];
199
214
  return;
200
215
  }
201
- BOOL content = waitForRender.hasValue;
216
+ BOOL bounded = !waitForRender.hasValue;
202
217
  __weak UIViewController *weakScreen = screen;
203
218
  NSArray<UIViewController *> * (^screens)(void) = ^NSArray<UIViewController *> * {
204
219
  UIViewController *waited = weakScreen;
205
220
  return waited ? waited.waitedComponentViewControllers : @[];
206
221
  };
207
- ANNRenderWaitContent waited = content ? ANNRenderWaitContentNode : ANNRenderWaitContentNone;
208
- ANNRenderWait *wait = [[ANNRenderWait alloc] initWithContent:waited
209
- node:content ? screen : nil
210
- screens:screens
211
- limit:0
212
- bar:ANNRenderWaitBarAfterContent
213
- barLimits:ANNColumnBarLimits];
214
- wait.unknownContentRule = content;
222
+ ANNRenderWait *wait =
223
+ [[ANNRenderWait alloc] initWithContent:ANNRenderWaitContentNode
224
+ node:screen
225
+ screens:screens
226
+ limit:bounded ? ANNColumnRenderLimit : 0
227
+ bar:ANNRenderWaitBarAfterContent
228
+ barLimits:ANNColumnBarLimits];
229
+ wait.unknownContentRule = !bounded;
230
+ wait.settlesThroughJavaScript = YES;
231
+ wait.barLimitsFromStart = bounded;
215
232
  __weak ANNColumnRequest *weakRequest = request;
216
233
  wait.canEnd = ^BOOL {
217
234
  ANNColumnRequest *waiting = weakRequest;
@@ -225,7 +242,7 @@ static NSMapTable<UIViewController *, ANNColumnRecordsOfSplitView *> *ANNColumnR
225
242
  if (!ready) {
226
243
  return;
227
244
  }
228
- // Without content the screen makes it right before it shows (as a push does).
245
+ // A screen that didn't render while parked makes its content right before it shows (as a push does).
229
246
  [ANNSplitViewCommands render:ready];
230
247
  if (ready.starting) {
231
248
  [ANNSplitViewCommands readyOnNextTurnOf:ready];
@@ -233,14 +250,12 @@ static NSMapTable<UIViewController *, ANNColumnRecordsOfSplitView *> *ANNColumnR
233
250
  [ANNSplitViewCommands readyOf:ready];
234
251
  }
235
252
  };
236
- if (content) {
237
- wait.setUp = ^{
238
- ANNColumnRequest *waiting = weakRequest;
239
- if (waiting) {
240
- [ANNSplitViewCommands park:waiting];
241
- }
242
- };
243
- }
253
+ wait.setUp = ^{
254
+ ANNColumnRequest *waiting = weakRequest;
255
+ if (waiting) {
256
+ [ANNSplitViewCommands park:waiting];
257
+ }
258
+ };
244
259
  request.wait = wait;
245
260
  request.starting = YES;
246
261
  @try {
@@ -281,6 +296,13 @@ static NSMapTable<UIViewController *, ANNColumnRecordsOfSplitView *> *ANNColumnR
281
296
  [screen setWaitForRender:YES];
282
297
  }
283
298
  if (window) {
299
+ __weak ANNColumnRequest *placed = request;
300
+ ANNScreenPlace * (^place)(void) = ^ANNScreenPlace * {
301
+ return [ANNSplitViewCommands placeOf:placed];
302
+ };
303
+ for (ANNComponentViewController *each in ANNStackScreensOfEntry(screen)) {
304
+ each.placeWhileParked = place;
305
+ }
284
306
  request.parking = [ANNParking new];
285
307
  [request.parking park:screen inView:window];
286
308
  }
@@ -304,6 +326,34 @@ static NSMapTable<UIViewController *, ANNColumnRecordsOfSplitView *> *ANNColumnR
304
326
  }
305
327
  }
306
328
 
329
+ // Where the parked screen will show, as far as it is known now: the column's stack (whose screens it replaces), the
330
+ // navigation controller UIKit keeps a column that is a screen alone in, or the source's stack while the split view
331
+ // shows as one stack. Nil without one: the screen lays out with its view's own safe area.
332
+ + (nullable ANNScreenPlace *)placeOf:(ANNColumnRequest *)request {
333
+ UIViewController *screen = request.screen;
334
+ UIViewController *source = request.source;
335
+ if (!screen || !source) {
336
+ return nil;
337
+ }
338
+ UIViewController *splitView = ANNSplitViewOf(source);
339
+ UINavigationController *stack = nil;
340
+ if (splitView && !ANNIsCollapsedIntoCompact(splitView)) {
341
+ UIViewController *columnNode =
342
+ [(id<ANNLayoutProtocol>)splitView viewControllerOfColumn:request.column];
343
+ stack = [columnNode isKindOfClass:UINavigationController.class]
344
+ ? (UINavigationController *)columnNode
345
+ : columnNode.navigationController;
346
+ } else {
347
+ stack = [ANNStackCommands stackOf:source].shownNavigationController;
348
+ }
349
+ if (!stack) {
350
+ return nil;
351
+ }
352
+ return [ANNScreenPlace placeOfEntry:screen
353
+ inStack:stack
354
+ options:[ANNStackOptionsScreen(screen) resolveOptionsWithDefault]];
355
+ }
356
+
307
357
  // The screen leaves its place and the waiting requests; UIKit ends its departure from the window here (it shows in
308
358
  // this turn, or goes).
309
359
  + (void)leaveWaitingPlace:(ANNColumnRequest *)request {
@@ -353,18 +403,23 @@ static NSMapTable<UIViewController *, ANNColumnRecordsOfSplitView *> *ANNColumnR
353
403
  [self request:request endedWithValue:nil rejection:text];
354
404
  }
355
405
 
356
- // Superseded at its readiness: its screen goes without having shown (no events), and its answer is the newest
357
- // request's, at once when that answered.
406
+ // Superseded at its readiness: its screen goes without having shown (no events). It answers when the newest request
407
+ // does, at once when that answered, with its own screen's id: the newest one's rejection (a container for a stack
408
+ // column, a split view that went) is about that request only.
358
409
  + (void)supersede:(ANNColumnRequest *)request {
359
410
  [self leaveWaitingPlace:request];
360
411
  request.done = YES;
412
+ NSString *value = ANNStackAnswerIdOf(request.screen, ANNStackAnswerCommandShowInColumn);
361
413
  [request.screen destroy];
362
414
  ANNColumnRecord *record = request.record;
363
415
  if (record.ended) {
364
- [self answer:request.answer value:record.value rejection:record.rejection];
416
+ [request.answer resolve:value];
365
417
  return;
366
418
  }
367
- [record.superseded addObject:request.answer];
419
+ ANNSupersededRequest *superseded = [ANNSupersededRequest new];
420
+ superseded.answer = request.answer;
421
+ superseded.value = value;
422
+ [record.superseded addObject:superseded];
368
423
  }
369
424
 
370
425
  // (a) The split view shows the screen in the column: a stack column's screens are replaced without an animation and
@@ -373,14 +428,26 @@ static NSMapTable<UIViewController *, ANNColumnRecordsOfSplitView *> *ANNColumnR
373
428
  // `DidDisappear` of the screens that left, `DidAppear` of the new one, the state, then the answer.
374
429
  + (void)show:(ANNColumnRequest *)request inSplitView:(UIViewController *)splitView {
375
430
  UIViewController *screen = request.screen;
431
+ UIViewController *columnNode =
432
+ [(id<ANNLayoutProtocol>)splitView viewControllerOfColumn:request.column];
433
+ // A stack column takes what a stack takes, a screen or a pair; any other layout rejects `columnIsStack`, as on
434
+ // Android. A column that is a screen alone takes any layout in its place.
435
+ if ([columnNode isKindOfClass:UINavigationController.class] &&
436
+ [ANNLayoutValidator checkStackChild:request.layout
437
+ stackId:columnNode.layoutInfo.componentId ?: @""]) {
438
+ [self leaveWaitingPlace:request];
439
+ [screen destroy];
440
+ [self request:request
441
+ endedWithValue:nil
442
+ rejection:[ANNErrorTexts columnIsStack:request.componentId column:request.column]];
443
+ return;
444
+ }
376
445
  // Before the parking lets go of it, so that UIKit's calls stay silent until the end.
377
446
  [ANNAppearanceDispatcher driveAppearanceOf:@[ screen ]];
378
447
  request.driven = YES;
379
448
  [self leaveWaitingPlace:request];
380
449
  // A screen alone in a column has the split view as its parent by the layout (UIKit puts it into a navigation
381
450
  // controller of its own); a stack column's stack is its parent.
382
- UIViewController *columnNode =
383
- [(id<ANNLayoutProtocol>)splitView viewControllerOfColumn:request.column];
384
451
  if (columnNode && ![columnNode isKindOfClass:UINavigationController.class]) {
385
452
  [screen setExplicitLayoutParent:splitView];
386
453
  }
@@ -485,7 +552,8 @@ static NSMapTable<UIViewController *, ANNColumnRecordsOfSplitView *> *ANNColumnR
485
552
 
486
553
  #pragma mark - The answers
487
554
 
488
- // The request answers once; when it is the newest of its column, the requests it superseded answer the same.
555
+ // The request answers once; when it is the newest of its column, the requests it superseded answer then, each with its
556
+ // own screen's id.
489
557
  + (void)request:(ANNColumnRequest *)request
490
558
  endedWithValue:(NSString *)value
491
559
  rejection:(NSString *)rejection {
@@ -499,13 +567,11 @@ static NSMapTable<UIViewController *, ANNColumnRecordsOfSplitView *> *ANNColumnR
499
567
  return;
500
568
  }
501
569
  record.ended = YES;
502
- record.value = value;
503
- record.rejection = rejection;
504
570
  record.newest = nil;
505
- NSArray<ANNCommandAnswer *> *superseded = [record.superseded copy];
571
+ NSArray<ANNSupersededRequest *> *superseded = [record.superseded copy];
506
572
  [record.superseded removeAllObjects];
507
- for (ANNCommandAnswer *answer in superseded) {
508
- [self answer:answer value:value rejection:rejection];
573
+ for (ANNSupersededRequest *each in superseded) {
574
+ [each.answer resolve:each.value];
509
575
  }
510
576
  // A request that begins from now on starts a record of its own.
511
577
  UIViewController *splitView = record.splitView;
@@ -214,19 +214,21 @@ static UISplitViewControllerColumn ANNSplitViewColumn(NSString *name) {
214
214
  completion:(ANNTransitionCompletionBlock)completion
215
215
  rejection:(ANNTransitionRejectionBlock)rejection {
216
216
  UIViewController *previous = _columns[column];
217
- NSString *fromId = fromViewController.layoutInfo.componentId;
217
+ NSString *fromId = fromViewController.layoutInfo.componentId ?: @"";
218
218
  if (!previous) {
219
219
  rejection([ANNErrorTexts noSuchColumn:fromId column:column], nil);
220
220
  return;
221
221
  }
222
222
  if ([previous isKindOfClass:UINavigationController.class]) {
223
- // UIKit does not take a navigation controller among the screens of another.
223
+ // UIKit does not take a navigation controller among the screens of another. The core rejects every layout a
224
+ // stack doesn't take before it asks; this guards the module's own call.
224
225
  if ([screen isKindOfClass:UINavigationController.class]) {
225
226
  rejection([ANNErrorTexts columnIsStack:fromId column:column], nil);
226
227
  return;
227
228
  }
228
229
  // In place, as a column of UIKit apps changes; UIKit animates a column it brings on top.
229
230
  [(UINavigationController *)previous setStackChildren:@[ screen ]
231
+ inColumn:column
230
232
  fromViewController:fromViewController
231
233
  animated:NO
232
234
  completion:^{
@@ -274,6 +274,7 @@
274
274
  if (target == stack.queue.entries.lastObject) {
275
275
  [stack.queue noneWithValue:componentId
276
276
  answer:answer
277
+ what:what
277
278
  componentId:componentId
278
279
  source:target];
279
280
  return;
@@ -1102,6 +1102,11 @@ static BOOL ANNIsActive(UIGestureRecognizer *recognizer) {
1102
1102
  if (change == ANNStackChangePush && animated) {
1103
1103
  lowerBarBottom = [self lowerBarBottomOf:navigation leaving:coveredTop];
1104
1104
  }
1105
+ // The large title of a screen that comes with its content takes its place for the transition before UIKit's push.
1106
+ if (change == ANNStackChangePush && animated) {
1107
+ UIViewController *arriving = ANNStackOptionsScreen(entries.lastObject);
1108
+ [[ANNTopBarPresenter presenterOfScreen:arriving] arrivesByPushOn:navigation];
1109
+ }
1105
1110
  // The revealed screen's large title takes its place for the transition before UIKit lays the screen out for it.
1106
1111
  if (change == ANNStackChangePop && animated) {
1107
1112
  [self popReveals:entries.lastObject leaving:coveredTop on:navigation];
@@ -1428,10 +1433,28 @@ static BOOL ANNIsActive(UIGestureRecognizer *recognizer) {
1428
1433
 
1429
1434
  // The screen waits in the window at the stack's size, under UIKit's container view ( `ANNParking`): it isn't a child of
1430
1435
  // anything there, gets no touches, is hidden from accessibility, and its appearance is driven, so UIKit's callbacks
1431
- // report nothing for it. Each path that ends the wait gives the view back as it was; UIKit's push of the entry
1432
- // re-parents it in the same turn, so its departure is not ended here.
1436
+ // report nothing for it. UIKit gives such a view no safe area: its screens lay out with that of their place on top of
1437
+ // the stack meanwhile (`ANNScreenPlace`). Each path that ends the wait gives the view back as it was; UIKit's push of
1438
+ // the entry re-parents it in the same turn, so its departure is not ended here.
1433
1439
  - (void)stackPark:(UIViewController *)entry {
1434
- [_parking park:entry inView:self.shownNavigationController.view];
1440
+ UINavigationController *navigation = self.shownNavigationController;
1441
+ // Its screens lay out as on top of the stack, with the bar and the toolbar the entry shows there.
1442
+ __weak UIViewController *weakEntry = entry;
1443
+ __weak UINavigationController *weakNavigation = navigation;
1444
+ ANNScreenPlace * (^place)(void) = ^ANNScreenPlace * {
1445
+ UIViewController *parked = weakEntry;
1446
+ UINavigationController *stack = weakNavigation;
1447
+ if (!parked || !stack) {
1448
+ return nil;
1449
+ }
1450
+ return [ANNScreenPlace placeOfEntry:parked
1451
+ inStack:stack
1452
+ options:[ANNStackOptionsScreen(parked) resolveOptionsWithDefault]];
1453
+ };
1454
+ for (ANNComponentViewController *screen in ANNStackScreensOfEntry(entry)) {
1455
+ screen.placeWhileParked = place;
1456
+ }
1457
+ [_parking park:entry inView:navigation.view];
1435
1458
  }
1436
1459
 
1437
1460
  - (void)stackUnpark:(UIViewController *)entry {
@@ -166,9 +166,11 @@ typedef NS_ENUM(NSInteger, ANNStackChange) {
166
166
  completion:(nullable dispatch_block_t)completion
167
167
  rejection:(nullable ANNTransitionRejectionBlock)rejection;
168
168
 
169
- /// A command that moves nothing (`popTo` of the top) answers `value` in its turn.
169
+ /// A command that moves nothing (`popTo` of the top) answers `value` in its turn; a failure meanwhile rejects with the
170
+ /// command's own `what`.
170
171
  - (void)noneWithValue:(id)value
171
172
  answer:(ANNCommandAnswer *)answer
173
+ what:(NSString *)what
172
174
  componentId:(NSString *)componentId
173
175
  source:(nullable UIViewController *)source;
174
176