@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
@@ -3,6 +3,7 @@
3
3
  #import "ANNIconButtonImage.h"
4
4
  #import "ANNShowTextButton.h"
5
5
  #import "ANNUIBarButtonItem.h"
6
+ #import "ANNUserChoice.h"
6
7
  #import "UIViewController+LayoutProtocol.h"
7
8
  #import <React/RCTLog.h>
8
9
 
@@ -54,6 +55,187 @@ static void ANNSetButtonsOfSide(ANNNavigationOptions *options, ANNBarButtonsSide
54
55
  }
55
56
  }
56
57
 
58
+ static ANNButtonOptions *ANNButtonWithId(NSArray<ANNButtonOptions *> *buttons, NSString *buttonId) {
59
+ for (ANNButtonOptions *button in buttons) {
60
+ if ([[button.identifier withDefault:nil] isEqualToString:buttonId]) {
61
+ return button;
62
+ }
63
+ }
64
+ return nil;
65
+ }
66
+
67
+ // The button `buttonId` of `side` in `options` changed on a copy, in a new list; nothing when the side has no such
68
+ // button. The other buttons stay the objects they are: nothing changes them.
69
+ static void ANNChangeButton(ANNNavigationOptions *options,
70
+ ANNBarButtonsSide side,
71
+ NSString *buttonId,
72
+ void (^change)(ANNButtonOptions *changed)) {
73
+ NSArray<ANNButtonOptions *> *list = ANNButtonsOfSide(options, side);
74
+ if (!ANNButtonWithId(list, buttonId)) {
75
+ return;
76
+ }
77
+ NSMutableArray<ANNButtonOptions *> *changedList = [NSMutableArray arrayWithCapacity:list.count];
78
+ for (ANNButtonOptions *button in list) {
79
+ if ([[button.identifier withDefault:nil] isEqualToString:buttonId]) {
80
+ ANNButtonOptions *copy = [button copy];
81
+ change(copy);
82
+ [changedList addObject:copy];
83
+ } else {
84
+ [changedList addObject:button];
85
+ }
86
+ }
87
+ ANNSetButtonsOfSide(options, side, changedList);
88
+ }
89
+
90
+ #pragma mark - The user's choices
91
+
92
+ static NSString *ANNToggleChoiceKey(ANNBarButtonsSide side, NSString *buttonId) {
93
+ return [NSString stringWithFormat:@"buttons.%ld.selected.%@", (long)side, buttonId];
94
+ }
95
+
96
+ static NSString *ANNButtonMenuChoiceKey(ANNBarButtonsSide side, NSString *buttonId) {
97
+ return [NSString stringWithFormat:@"buttons.%ld.menu.%@", (long)side, buttonId];
98
+ }
99
+
100
+ // The button `buttonId` of `side` in `options`, or nil.
101
+ static ANNButtonOptions *ANNButtonIn(ANNNavigationOptions *options, ANNBarButtonsSide side,
102
+ NSString *buttonId) {
103
+ return ANNButtonWithId(ANNButtonsOfSide(options, side), buttonId);
104
+ }
105
+
106
+ // A toggle the user flipped: `selected` of the button with its id, wherever its list comes from. It goes when the
107
+ // node's resolution after a merge has no such button, or when the merge gives the button a `selected` other than the
108
+ // app had (the own button's, else the button the node resolved to before the merge), by the rules of a menu's check.
109
+ static ANNUserChoice *ANNToggleChoice(ANNBarButtonsSide side, NSString *buttonId, BOOL selected) {
110
+ return [ANNUserChoice choiceWithValue:@(selected)
111
+ apply:^(ANNNavigationOptions *options, id value) {
112
+ BOOL on = [(NSNumber *)value boolValue];
113
+ ANNChangeButton(options, side, buttonId, ^(ANNButtonOptions *changed) {
114
+ changed.selected = [Bool withValue:on];
115
+ });
116
+ }
117
+ merged:^id(id value, ANNNavigationOptions *merge, ANNNavigationOptions *own,
118
+ ANNNavigationOptions *before, ANNNavigationOptions *after) {
119
+ if (!ANNButtonIn(after, side, buttonId)) {
120
+ return nil;
121
+ }
122
+ ANNButtonOptions *given = ANNButtonIn(merge, side, buttonId);
123
+ if (!given.selected.hasValue) {
124
+ return value;
125
+ }
126
+ ANNButtonOptions *was = ANNButtonIn(own, side, buttonId);
127
+ if (!was.selected.hasValue) {
128
+ was = ANNButtonIn(before, side, buttonId);
129
+ }
130
+ BOOL same = [given.selected withDefault:NO] == [was.selected withDefault:NO];
131
+ return same ? value : nil;
132
+ }];
133
+ }
134
+
135
+ // The items of the level (a menu's or a submenu's own list, without its submenus) that holds `itemId`, at any depth;
136
+ // nil when no level holds it.
137
+ static NSArray<ANNMenuItemOptions *> *ANNLevelOf(NSArray<ANNMenuItemOptions *> *items,
138
+ NSString *itemId) {
139
+ NSMutableArray<ANNMenuItemOptions *> *level = [NSMutableArray new];
140
+ BOOL holds = NO;
141
+ for (ANNMenuItemOptions *item in items) {
142
+ NSString *leafId = item.items.count ? nil : [item.identifier withDefault:nil];
143
+ if (leafId) {
144
+ [level addObject:item];
145
+ holds = holds || [leafId isEqualToString:itemId];
146
+ }
147
+ }
148
+ if (holds) {
149
+ return level;
150
+ }
151
+ for (ANNMenuItemOptions *item in items) {
152
+ if (item.items.count) {
153
+ NSArray<ANNMenuItemOptions *> *found = ANNLevelOf(item.items, itemId);
154
+ if (found) {
155
+ return found;
156
+ }
157
+ }
158
+ }
159
+ return nil;
160
+ }
161
+
162
+ // The ids of the items in the level that holds `itemId`: the neighbors one pick unchecks.
163
+ static NSArray<NSString *> *ANNNeighborsOf(NSArray<ANNMenuItemOptions *> *items, NSString *itemId) {
164
+ NSMutableArray<NSString *> *ids = [NSMutableArray new];
165
+ for (ANNMenuItemOptions *item in ANNLevelOf(items, itemId)) {
166
+ [ids addObject:[item.identifier withDefault:@""]];
167
+ }
168
+ return ids;
169
+ }
170
+
171
+ // The checked items of the level that holds `itemId` in `menu`: the field a merge gives that level. Nil when `menu`
172
+ // doesn't hold the item.
173
+ static NSSet<NSString *> *ANNCheckedInLevel(ANNMenuOptions *menu, NSString *itemId) {
174
+ NSArray<ANNMenuItemOptions *> *level = menu ? ANNLevelOf(menu.items, itemId) : nil;
175
+ if (!level) {
176
+ return nil;
177
+ }
178
+ NSMutableSet<NSString *> *checked = [NSMutableSet new];
179
+ for (ANNMenuItemOptions *item in level) {
180
+ if ([item.checked withDefault:NO]) {
181
+ [checked addObject:[item.identifier withDefault:@""]];
182
+ }
183
+ }
184
+ return checked;
185
+ }
186
+
187
+ // A menu that has items, or nil: a merge without items gives no menu.
188
+ static ANNMenuOptions *ANNMenuWithItems(ANNMenuOptions *menu) {
189
+ return menu.items ? menu : nil;
190
+ }
191
+
192
+ // The picks of single-selection levels in one menu, in the order made: the resolution plays them again on a copy of
193
+ // the menu it has, so a new menu from the app keeps the check where its items have the same ids. As on Android
194
+ // (`UserChoices.yieldTo`), at each merge into the node a pick goes when the node's resolution after it no longer has
195
+ // the item, or when the merge gives the menu with other checks in the pick's level than the app had there (the own
196
+ // options' menu, else the menu the node resolved to before the merge); the picks of the other levels stay.
197
+ typedef ANNMenuOptions *_Nullable (^ANNMenuOf)(ANNNavigationOptions *options);
198
+ typedef void (^ANNSetMenu)(ANNNavigationOptions *options, ANNMenuOptions *menu);
199
+
200
+ static ANNUserChoice *ANNMenuChoice(NSArray<NSString *> *picks,
201
+ ANNMenuOf menuOf,
202
+ ANNSetMenu setMenu) {
203
+ return [ANNUserChoice choiceWithValue:picks
204
+ apply:^(ANNNavigationOptions *options, id value) {
205
+ ANNMenuOptions *menu = menuOf(options);
206
+ if (menu.items.count == 0) {
207
+ return;
208
+ }
209
+ ANNMenuOptions *copy = [ANNBarButtons deepCopyOfMenu:menu];
210
+ for (NSString *itemId in (NSArray<NSString *> *)value) {
211
+ [copy itemPicked:itemId];
212
+ }
213
+ setMenu(options, copy);
214
+ }
215
+ merged:^id(id value, ANNNavigationOptions *merge, ANNNavigationOptions *own,
216
+ ANNNavigationOptions *before, ANNNavigationOptions *after) {
217
+ ANNMenuOptions *now = ANNMenuWithItems(menuOf(after));
218
+ ANNMenuOptions *given = ANNMenuWithItems(menuOf(merge));
219
+ ANNMenuOptions *was = ANNMenuWithItems(menuOf(own)) ?: ANNMenuWithItems(menuOf(before));
220
+ NSArray<NSString *> *made = value;
221
+ NSMutableArray<NSString *> *kept = [NSMutableArray new];
222
+ for (NSString *itemId in made) {
223
+ BOOL gone = !now || !ANNLevelOf(now.items, itemId);
224
+ NSSet<NSString *> *givenChecks = given ? ANNCheckedInLevel(given, itemId) : nil;
225
+ NSSet<NSString *> *wasChecks = ANNCheckedInLevel(was, itemId);
226
+ BOOL same = givenChecks == wasChecks || [givenChecks isEqualToSet:wasChecks];
227
+ BOOL changed = given && !same;
228
+ if (!gone && !changed) {
229
+ [kept addObject:itemId];
230
+ }
231
+ }
232
+ if (kept.count == 0) {
233
+ return nil;
234
+ }
235
+ return kept.count == made.count ? value : kept;
236
+ }];
237
+ }
238
+
57
239
  static UIColor *ANNColorOf(Color *color) {
58
240
  if (!color.hasValue || color.isNoColor) {
59
241
  return nil;
@@ -438,11 +620,12 @@ static UIBarButtonSystemItem ANNSystemItemOf(NSString *name) {
438
620
  }
439
621
  UIMenu *built = [current menuWithPickHandler:^(NSString *itemId) {
440
622
  [weakSelf menuItemPicked:itemId
441
- menuOf:^ANNMenuOptions *(ANNNavigationOptions *resolved) {
442
- return resolved.topBar.overflowMenu;
623
+ choiceKey:@"topBar.overflowMenu"
624
+ menuOf:^ANNMenuOptions *(ANNNavigationOptions *options) {
625
+ return options.topBar.overflowMenu;
443
626
  }
444
- setMenu:^(ANNNavigationOptions *own, ANNMenuOptions *copy) {
445
- own.topBar.overflowMenu = copy;
627
+ setMenu:^(ANNNavigationOptions *options, ANNMenuOptions *copy) {
628
+ options.topBar.overflowMenu = copy;
446
629
  }];
447
630
  }];
448
631
  UIMenu *section = [UIMenu menuWithTitle:built.title
@@ -880,16 +1063,14 @@ static UIBarButtonSystemItem ANNSystemItemOf(NSString *name) {
880
1063
  __weak ANNBarButtons *weakSelf = self;
881
1064
  item.menu = [menu menuWithPickHandler:^(NSString *itemId) {
882
1065
  [weakSelf menuItemPicked:itemId
1066
+ choiceKey:ANNButtonMenuChoiceKey(side, buttonId)
883
1067
  menuOf:^ANNMenuOptions *(ANNNavigationOptions *options) {
884
- return [ANNBarButtons buttonWithId:buttonId in:ANNButtonsOfSide(options, side)].menu;
1068
+ return ANNButtonWithId(ANNButtonsOfSide(options, side), buttonId).menu;
885
1069
  }
886
- setMenu:^(ANNNavigationOptions *own, ANNMenuOptions *copy) {
887
- [weakSelf changeButton:buttonId
888
- side:side
889
- inOwn:own
890
- change:^(ANNButtonOptions *changed) {
891
- changed.menu = copy;
892
- }];
1070
+ setMenu:^(ANNNavigationOptions *options, ANNMenuOptions *copy) {
1071
+ ANNChangeButton(options, side, buttonId, ^(ANNButtonOptions *changed) {
1072
+ changed.menu = copy;
1073
+ });
893
1074
  }];
894
1075
  }];
895
1076
  if (![button.menuOnLongPress withDefault:NO]) {
@@ -948,22 +1129,15 @@ static UIBarButtonSystemItem ANNSystemItemOf(NSString *name) {
948
1129
  [screen.eventEmitter sendOnNavigationButtonPressed:componentId buttonId:press.buttonId];
949
1130
  return;
950
1131
  }
951
- // A toggle flips, the options keep the new state, then the event tells it.
1132
+ // A toggle flips, the user layer keeps the new state, then the event tells it.
952
1133
  ANNUIBarButtonItem *item = press.item;
953
1134
  BOOL selected = !item.selected;
954
1135
  item.selected = selected;
955
1136
  _selected[press.buttonId] = @(selected);
956
- __weak ANNBarButtons *weakSelf = self;
957
1137
  NSString *buttonId = press.buttonId;
958
1138
  ANNBarButtonsSide side = press.side;
959
- [screen changeOwnOptions:^(ANNNavigationOptions *own) {
960
- [weakSelf changeButton:buttonId
961
- side:side
962
- inOwn:own
963
- change:^(ANNButtonOptions *changed) {
964
- changed.selected = [Bool withValue:selected];
965
- }];
966
- }];
1139
+ [screen setUserChoice:ANNToggleChoice(side, buttonId, selected)
1140
+ forKey:ANNToggleChoiceKey(side, buttonId)];
967
1141
  [screen.eventEmitter sendOnNavigationButtonPressed:componentId
968
1142
  buttonId:buttonId
969
1143
  selected:@(selected)];
@@ -971,46 +1145,15 @@ static UIBarButtonSystemItem ANNSystemItemOf(NSString *name) {
971
1145
 
972
1146
  + (nullable ANNButtonOptions *)buttonWithId:(NSString *)buttonId
973
1147
  in:(NSArray<ANNButtonOptions *> *)buttons {
974
- for (ANNButtonOptions *button in buttons) {
975
- if ([[button.identifier withDefault:nil] isEqualToString:buttonId]) {
976
- return button;
977
- }
978
- }
979
- return nil;
980
- }
981
-
982
- // The button `buttonId` of `side` in the own options, changed on a copy: the own list when it has the button, else the
983
- // list the screen shows (from its stack or the defaults), copied into the own options first. Lists are new arrays.
984
- - (void)changeButton:(NSString *)buttonId
985
- side:(ANNBarButtonsSide)side
986
- inOwn:(ANNNavigationOptions *)own
987
- change:(void (^)(ANNButtonOptions *changed))change {
988
- NSArray<ANNButtonOptions *> *list = ANNButtonsOfSide(own, side);
989
- if (![ANNBarButtons buttonWithId:buttonId in:list]) {
990
- list = ANNButtonsOfSide([_screen resolveOptionsWithDefault], side);
991
- }
992
- if (![ANNBarButtons buttonWithId:buttonId in:list]) {
993
- return;
994
- }
995
- NSMutableArray<ANNButtonOptions *> *changedList = [NSMutableArray arrayWithCapacity:list.count];
996
- for (ANNButtonOptions *button in list) {
997
- ANNButtonOptions *copy = [button copy];
998
- if (button.menu.items.count > 0) {
999
- copy.menu = [ANNBarButtons deepCopyOfMenu:button.menu];
1000
- }
1001
- if ([[button.identifier withDefault:nil] isEqualToString:buttonId]) {
1002
- change(copy);
1003
- }
1004
- [changedList addObject:copy];
1005
- }
1006
- ANNSetButtonsOfSide(own, side, changedList);
1148
+ return ANNButtonWithId(buttons, buttonId);
1007
1149
  }
1008
1150
 
1009
1151
  #pragma mark - Menus
1010
1152
 
1011
1153
  - (void)menuItemPicked:(NSString *)itemId
1154
+ choiceKey:(NSString *)choiceKey
1012
1155
  menuOf:(ANNMenuOptions *_Nullable (^)(ANNNavigationOptions *options))menuOf
1013
- setMenu:(void (^)(ANNNavigationOptions *own, ANNMenuOptions *menu))setMenu {
1156
+ setMenu:(void (^)(ANNNavigationOptions *options, ANNMenuOptions *menu))setMenu {
1014
1157
  UIViewController *screen = _screen;
1015
1158
  NSString *componentId = screen.layoutInfo.componentId;
1016
1159
  if (!screen || componentId.length == 0) {
@@ -1021,13 +1164,18 @@ static UIBarButtonSystemItem ANNSystemItemOf(NSString *name) {
1021
1164
  if (!menu || ![ANNBarButtons menu:menu hasSingleSelectionOf:itemId]) {
1022
1165
  return;
1023
1166
  }
1024
- // The check lands on the screen's own copy, which shares nothing with the menus of the stack, the defaults or
1025
- // other screens.
1026
- ANNMenuOptions *copy = [ANNBarButtons deepCopyOfMenu:menu];
1027
- [copy itemPicked:itemId];
1028
- [screen changeOwnOptions:^(ANNNavigationOptions *own) {
1029
- setMenu(own, copy);
1030
- }];
1167
+ // The new pick goes last; the earlier picks of its own list go, so a pick the app takes away later doesn't bring
1168
+ // back one the user replaced.
1169
+ NSArray<NSString *> *made = [screen userChoiceForKey:choiceKey].value ?: @[];
1170
+ NSArray<NSString *> *neighbors = ANNNeighborsOf(menu.items, itemId);
1171
+ NSMutableArray<NSString *> *picks = [NSMutableArray new];
1172
+ for (NSString *pick in made) {
1173
+ if (![neighbors containsObject:pick]) {
1174
+ [picks addObject:pick];
1175
+ }
1176
+ }
1177
+ [picks addObject:itemId];
1178
+ [screen setUserChoice:ANNMenuChoice(picks, menuOf, setMenu) forKey:choiceKey];
1031
1179
  }
1032
1180
 
1033
1181
  + (BOOL)menu:(ANNMenuOptions *)menu hasSingleSelectionOf:(NSString *)itemId {
@@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
13
13
 
14
14
  /// `tint` colors the icon (nil: its own colors); `background` fills the rectangle of `size` (zero: the icon's size)
15
15
  /// rounded by `cornerRadius`; `insets` pad the icon. Colors may be dynamic: each variant resolves them.
16
+ /// An icon that can't be drawn so (no room, an exception while drawing) comes back as it is.
16
17
  + (UIImage *)imageWithIcon:(UIImage *)icon
17
18
  tint:(nullable UIColor *)tint
18
19
  background:(nullable UIColor *)background
@@ -1,5 +1,6 @@
1
1
  #import "ANNIconButtonImage.h"
2
2
  #import "ANNUtils.h"
3
+ #import <React/RCTLog.h>
3
4
 
4
5
  @implementation ANNIconButtonImage
5
6
 
@@ -9,6 +10,30 @@
9
10
  cornerRadius:(CGFloat)cornerRadius
10
11
  size:(CGSize)size
11
12
  insets:(UIEdgeInsets)insets {
13
+ // An icon that can't be drawn so (no size, insets that leave no room, an exception of the renderer) shows as it is.
14
+ @try {
15
+ UIImage *image = [self variantsOfIcon:icon
16
+ tint:tint
17
+ background:background
18
+ cornerRadius:cornerRadius
19
+ size:size
20
+ insets:insets];
21
+ if (image) {
22
+ return image;
23
+ }
24
+ } @catch (NSException *exception) {
25
+ RCTLogWarn(@"[ApexNavigation] a button's image could not be drawn on its background: %@",
26
+ exception.reason);
27
+ }
28
+ return icon;
29
+ }
30
+
31
+ + (nullable UIImage *)variantsOfIcon:(UIImage *)icon
32
+ tint:(UIColor *)tint
33
+ background:(UIColor *)background
34
+ cornerRadius:(CGFloat)cornerRadius
35
+ size:(CGSize)size
36
+ insets:(UIEdgeInsets)insets {
12
37
  // Each variant carries a scale with its style: an asset of style-only traits gives back an image of scale 1, its
13
38
  // pixels taken as points (a 32 pt background drawn 96 pt wide). The current scale and the usual ones are drawn,
14
39
  // so a screen of another scale still finds its style. Each variant is an original image, so that a variant UIKit
@@ -30,6 +55,9 @@
30
55
  size:size
31
56
  insets:insets
32
57
  traits:traits];
58
+ if (!drawn) {
59
+ return nil;
60
+ }
33
61
  [asset registerImage:[drawn imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]
34
62
  withTraitCollection:traits];
35
63
  }
@@ -54,7 +82,7 @@
54
82
  ]];
55
83
  }
56
84
 
57
- + (UIImage *)drawIcon:(UIImage *)icon
85
+ + (nullable UIImage *)drawIcon:(UIImage *)icon
58
86
  tint:(UIColor *)tint
59
87
  background:(UIColor *)background
60
88
  cornerRadius:(CGFloat)cornerRadius
@@ -66,6 +94,11 @@
66
94
  iconSize.height + insets.top + insets.bottom);
67
95
  CGSize canvas = CGSizeMake(size.width > 0 ? size.width : padded.width,
68
96
  size.height > 0 ? size.height : padded.height);
97
+ BOOL drawable = canvas.width > 0 && canvas.height > 0 && isfinite(canvas.width) &&
98
+ isfinite(canvas.height);
99
+ if (!drawable) {
100
+ return nil;
101
+ }
69
102
  UIImage *drawn = icon;
70
103
  if (tint) {
71
104
  drawn = [icon imageWithTintColor:[tint resolvedColorWithTraitCollection:traits]
@@ -57,6 +57,19 @@ typedef struct {
57
57
  /// goes, and the bar follows what it follows at rest.
58
58
  - (void)screenDidAppear:(ANNNavigationOptions *)options list:(nullable UIScrollView *)list;
59
59
 
60
+ /// iOS 26: a push starts that brings the screen in with its content drawn already (it waited for it parked, out of the
61
+ /// stack). UIKit draws the large title, for the whole transition, inside the scroll view the bar follows, and takes
62
+ /// its place from that scroll view as the transition starts: a list below the bar clips it, a list laid out before the
63
+ /// push may give it the room of a small title, and a title that doesn't fold into the list has nothing to follow yet.
64
+ /// The screen's view gets the place of `screenInPop:list:inlineInRow:`, at rest at `top`, the top of the safe area the
65
+ /// screen takes on the stack (`stackTop` the stack's own), and the bar follows it until the screen appeared, also over
66
+ /// a main list. Called before UIKit's push.
67
+ - (void)screenInPush:(ANNNavigationOptions *)options
68
+ list:(nullable UIScrollView *)list
69
+ inlineInRow:(BOOL)inlineInRow
70
+ top:(CGFloat)top
71
+ stackTop:(CGFloat)stackTop;
72
+
60
73
  /// The screen went without appearing (a swipe back given up): the place of the title of the pop goes, and the bar
61
74
  /// follows what it followed before.
62
75
  - (void)screenDidDisappear:(ANNNavigationOptions *)options list:(nullable UIScrollView *)list;
@@ -37,9 +37,11 @@ static UINavigationItemLargeTitleDisplayMode ANNItemModeOf(NSString *mode) {
37
37
  // The push from a lower bar whose fill is pending: the old bar's bottom in the screen's coordinates.
38
38
  BOOL _titlePlacePending;
39
39
  CGFloat _oldBarBottom;
40
- // The place of the title while a pop reveals the screen, and whether the bar follows it now.
40
+ // The place of the title while a pop reveals the screen, and whether the bar follows it now; for a push, the bar
41
+ // follows it until the screen appeared, also with a main list.
41
42
  UIScrollView *_titleHost;
42
43
  BOOL _hostsTitle;
44
+ BOOL _hostsPushTitle;
43
45
  // The largest room the bar took over the content while the screen showed at rest: that of an expanded title.
44
46
  CGFloat _expandedRoom;
45
47
  }
@@ -99,13 +101,15 @@ static UINavigationItemLargeTitleDisplayMode ANNItemModeOf(NSString *mode) {
99
101
  if (followed != _emptyScrollView) {
100
102
  _emptyScrollView = nil;
101
103
  }
102
- // The place of a pop's title holds while the title can show and no main list at the top takes it over.
104
+ // The place of a pop's title holds while the title can show and no main list at the top takes it over; that of a
105
+ // push, while the title can show.
103
106
  BOOL dropsHost = NO;
104
107
  if (_hostsTitle) {
105
- if (canShow && (!collapses || !list)) {
108
+ if (canShow && (_hostsPushTitle || !collapses || !list)) {
106
109
  followed = _titleHost;
107
110
  } else {
108
111
  _hostsTitle = NO;
112
+ _hostsPushTitle = NO;
109
113
  dropsHost = YES;
110
114
  }
111
115
  }
@@ -159,6 +163,23 @@ static UINavigationItemLargeTitleDisplayMode ANNItemModeOf(NSString *mode) {
159
163
  }
160
164
  }
161
165
 
166
+ - (void)screenInPush:(ANNNavigationOptions *)options
167
+ list:(UIScrollView *)list
168
+ inlineInRow:(BOOL)inlineInRow
169
+ top:(CGFloat)top
170
+ stackTop:(CGFloat)stackTop {
171
+ if (@available(iOS 26.0, *)) {
172
+ UIView *screenView = _screen.viewIfLoaded;
173
+ if (!screenView || inlineInRow || !ANNLargeTitleCanShow(options) || top - stackTop <= 0) {
174
+ return;
175
+ }
176
+ [self placeTitleHostIn:screenView top:top];
177
+ _hostsTitle = YES;
178
+ _hostsPushTitle = YES;
179
+ [self applyCollapse:options list:list];
180
+ }
181
+ }
182
+
162
183
  // A plain scroll view over the whole screen, at rest at its top under the bar (its content starts at `top`, where the
163
184
  // content of the screen started as it was covered): UIKit puts the title just above that start, where the bar's title
164
185
  // is at rest. It takes no touches and draws nothing itself; the core's search of the screen's list skips it.
@@ -195,6 +216,7 @@ static UINavigationItemLargeTitleDisplayMode ANNItemModeOf(NSString *mode) {
195
216
  - (void)handBackTitle:(ANNNavigationOptions *)options list:(UIScrollView *)list {
196
217
  BOOL hosted = _hostsTitle;
197
218
  _hostsTitle = NO;
219
+ _hostsPushTitle = NO;
198
220
  [self applyCollapse:options list:list];
199
221
  if (hosted) {
200
222
  [_titleHost removeFromSuperview];
@@ -252,6 +274,7 @@ static UINavigationItemLargeTitleDisplayMode ANNItemModeOf(NSString *mode) {
252
274
 
253
275
  - (void)removeTitleHost {
254
276
  _hostsTitle = NO;
277
+ _hostsPushTitle = NO;
255
278
  [_titleHost removeFromSuperview];
256
279
  }
257
280