@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
@@ -2,7 +2,6 @@
2
2
  #import "ANNOptionsReader.h"
3
3
  #import <React/RCTConvert.h>
4
4
  #import <React/RCTLog.h>
5
- #import <React/RCTUtils.h>
6
5
 
7
6
  @implementation ImageParser
8
7
 
@@ -12,10 +11,10 @@
12
11
  return [[Image alloc] initWithValue:nil];
13
12
  }
14
13
  NSString *path = [ANNOptionsReader pathOfKey:key];
15
- return [[Image alloc] initWithValue:[self imageOf:value path:path]];
14
+ return [[Image alloc] initWithValue:[self sourceOf:value path:path]];
16
15
  }
17
16
 
18
- + (UIImage *)imageOf:(id)value path:(NSString *)path {
17
+ + (ANNImageSource *)sourceOf:(id)value path:(NSString *)path {
19
18
  if ([value isKindOfClass:NSArray.class]) {
20
19
  RCTLogError(@"[ApexNavigation] the image of '%@' is a list of sources, which options don't support: give "
21
20
  @"one source",
@@ -23,27 +22,28 @@
23
22
  return nil;
24
23
  }
25
24
  if ([value isKindOfClass:NSDictionary.class] && value[@"system"]) {
26
- return [self systemImageOf:value path:path];
25
+ return [self symbolOf:value path:path];
27
26
  }
28
27
  if (![value isKindOfClass:NSString.class] && ![value isKindOfClass:NSDictionary.class]) {
29
28
  return nil;
30
29
  }
31
- return [self imageOfSource:value path:path];
30
+ return [self addressOf:value path:path];
32
31
  }
33
32
 
34
33
  // An SF Symbol; a name the system doesn't have takes the fallback source, read the same way.
35
- + (UIImage *)systemImageOf:(NSDictionary *)value path:(NSString *)path {
34
+ + (ANNImageSource *)symbolOf:(NSDictionary *)value path:(NSString *)path {
36
35
  id name = value[@"system"];
37
- UIImage *image = [name isKindOfClass:NSString.class] ? [UIImage systemImageNamed:name] : nil;
38
- if (image) {
39
- return image;
40
- }
41
36
  id fallback = value[@"fallback"];
42
- return fallback && fallback != NSNull.null ? [self imageOf:fallback path:path] : nil;
37
+ ANNImageSource *fallbackSource =
38
+ fallback && fallback != NSNull.null ? [self sourceOf:fallback path:path] : nil;
39
+ if (![name isKindOfClass:NSString.class] || [name length] == 0) {
40
+ return fallbackSource;
41
+ }
42
+ return [ANNImageSource symbolNamed:name fallback:fallbackSource];
43
43
  }
44
44
 
45
- // A resolved source, loaded synchronously as React Native's converter of images does, with the scale of the source.
46
- + (UIImage *)imageOfSource:(id)source path:(NSString *)path {
45
+ // A resolved source: its address and the fields its scale comes from.
46
+ + (ANNImageSource *)addressOf:(id)source path:(NSString *)path {
47
47
  NSDictionary *fields = [source isKindOfClass:NSDictionary.class] ? source : nil;
48
48
  id address = fields ? (fields[@"uri"] ?: fields[@"url"]) : source;
49
49
  if (![address isKindOfClass:NSString.class] || [address length] == 0) {
@@ -56,48 +56,30 @@
56
56
  }
57
57
  NSURL *url = [RCTConvert NSURL:address];
58
58
  NSString *scheme = url.scheme.lowercaseString;
59
- BOOL metroAsset = ANNJSONIsBoolean(fields[@"__packager_asset"]) && [fields[@"__packager_asset"] boolValue];
60
- UIImage *image = nil;
59
+ id packagerAsset = fields[@"__packager_asset"];
60
+ BOOL metroAsset = ANNJSONIsBoolean(packagerAsset) && [packagerAsset boolValue];
61
+ ANNImageSourceKind kind;
61
62
  if ([scheme isEqualToString:@"file"]) {
62
- image = RCTImageFromLocalAssetURL(url) ?: RCTImageFromLocalBundleAssetURL(url);
63
+ kind = ANNImageSourceKindFile;
63
64
  } else if ([scheme isEqualToString:@"data"]) {
64
- image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
65
+ kind = ANNImageSourceKindData;
65
66
  } else if (([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"]) && metroAsset) {
66
- // Metro's assets in development: loaded from the dev server, synchronously.
67
- image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
67
+ kind = ANNImageSourceKindMetro;
68
68
  } else {
69
69
  RCTLogError(@"[ApexNavigation] the image of '%@' is not loaded: remote images are not supported in options "
70
70
  @"(%@)",
71
71
  path, address);
72
72
  return nil;
73
73
  }
74
- if (!image) {
75
- RCTLogError(@"[ApexNavigation] the image of '%@' could not be loaded: %@", path, address);
76
- return nil;
77
- }
78
- return [self image:image scaledBy:fields];
79
- }
80
-
81
- // The scale of the source; without it the pixel width over `width`; without both, the file's own. A size that
82
- // doesn't match is only logged.
83
- + (UIImage *)image:(UIImage *)image scaledBy:(NSDictionary *)fields {
84
- id scaleValue = fields[@"scale"];
85
- id widthValue = fields[@"width"];
86
- id heightValue = fields[@"height"];
87
- CGFloat scale = ANNJSONIsNumber(scaleValue) ? [scaleValue doubleValue] : 0;
88
- CGFloat width = ANNJSONIsNumber(widthValue) ? [widthValue doubleValue] : 0;
89
- CGFloat height = ANNJSONIsNumber(heightValue) ? [heightValue doubleValue] : 0;
90
- if (scale <= 0 && width > 0 && image.CGImage) {
91
- scale = CGImageGetWidth(image.CGImage) / width;
92
- }
93
- if (scale > 0 && image.CGImage && scale != image.scale) {
94
- image = [UIImage imageWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
95
- }
96
- if (width > 0 && height > 0 && !CGSizeEqualToSize(image.size, CGSizeMake(width, height))) {
97
- RCTLogInfo(@"[ApexNavigation] an image of the options is %@, its source says %@",
98
- NSStringFromCGSize(image.size), NSStringFromCGSize(CGSizeMake(width, height)));
99
- }
100
- return image;
74
+ id scale = fields[@"scale"];
75
+ id width = fields[@"width"];
76
+ id height = fields[@"height"];
77
+ return [ANNImageSource sourceOfKind:kind
78
+ url:url
79
+ address:address
80
+ scale:ANNJSONIsNumber(scale) ? [scale doubleValue] : 0
81
+ width:ANNJSONIsNumber(width) ? [width doubleValue] : 0
82
+ height:ANNJSONIsNumber(height) ? [height doubleValue] : 0];
101
83
  }
102
84
 
103
85
  @end
@@ -37,7 +37,7 @@ NS_SWIFT_NAME(NativeScreenContext)
37
37
 
38
38
  /**
39
39
  * Runs a command of the navigation from this screen in JS, as `useNavigation()` of a React screen does: push, pop,
40
- * popTo, popToRoot, showInColumn, showModal, dismiss, showOverlay, dismissOverlay, setOptions, selectTab, setTabBadge.
40
+ * popTo, popToRoot, showInColumn, showModal, dismiss, showOverlay, setOptions, selectTab, setTabBadge.
41
41
  * `arguments` are those of the JS command; `completion` gets what it resolved with.
42
42
  *
43
43
  * ```swift
package/ios/RECIPES.md CHANGED
@@ -16,9 +16,14 @@ you do exactly that; the map is `ios/CLAUDE.md`.
16
16
  what the merge names) and in `applyDefaultOptionsChange:`. A system-UI option also goes into
17
17
  `+[ANNSystemUI groupsNamedBy:onlyRead:]`; a bar-placement option into `ANNOptionsChangeScreenBars`.
18
18
  4. **The cache.** Never invalidate by hand. Own options change through `ANNNodeHelper` (`mergeOwnOptions:`,
19
- `changeOwnOptions:`) and defaults through `+[ANNDefaultOptions setCurrent:]`; both call
20
- `+[ANNOptionsCache optionsChanged]`. A presenter or module that keeps a choice of the user writes it with
21
- `changeOwnOptions:`, never into the options in place.
19
+ `changeOwnOptions:`), the user layer through `setUserChoice:forKey:`, and defaults through
20
+ `+[ANNDefaultOptions setCurrent:]`; all call `+[ANNOptionsCache optionsChanged]`. `changeOwnOptions:` is for what
21
+ the core keeps of the app's merges. A presenter or module that keeps a choice of the user (a toggle, a menu check,
22
+ a scope) puts an `ANNUserChoice` into the node's user layer under a key of its own: its `apply` block writes the
23
+ choice by identity (a button or item by its id) on a copy, its `merged` block says when the app's merge takes it
24
+ away, by Android's rules (`UserChoices.yieldTo`): a value other than the app had there (the own options', else the
25
+ node's resolution before the merge), or what the choice is about gone from the resolution after it. Never into the
26
+ options in place.
22
27
  5. **Android** reads the same JSON and has unit tests of parsing and merging; the iOS side has none (see Checks in `ios/CLAUDE.md`).
23
28
  6. **The rule:** an option does what its name promises, with the same meaning on both platforms
24
29
  (`.claude/rules/general.md`, `.claude/rules/options.md`). A platform that can't, says so in the JSDoc.
@@ -37,6 +42,7 @@ you do exactly that; the map is `ios/CLAUDE.md`.
37
42
  thrown with `+[ANNErrorTexts rejection:]`. A new text goes into `src/interfaces/contract/errors.json`, and
38
43
  `node scripts/contract.mjs` makes `ANNErrorTexts` (and Android's `ErrorTexts`) from it; never edit them by hand.
39
44
  6. **The answer:** end through `ANNAppearanceDispatcher` (`endTransitionHiding:…answer:value:`) or call
40
- `-[ANNCommandAnswer resolve:]`; the state and `ANN.CommandCompleted` follow by themselves for the names in
41
- `ANNCompletedCommandNames`. Add the name there when the command shows or dismisses.
45
+ `-[ANNCommandAnswer resolve:]`; the state and `ANN.CommandCompleted` follow by themselves as `ANNCommandContract`
46
+ says. A new command with a promise gets its entry in `src/interfaces/contract/commands.json` (what it resolves
47
+ with, `state`, `completed`), then `node scripts/contract.mjs`.
42
48
  7. **Android** gets the same command; its tests check the texts.
@@ -8,11 +8,13 @@
8
8
 
9
9
  using namespace facebook::react;
10
10
 
11
+ @interface ANNSlotComponentView () <ANNSlotView>
12
+ @end
13
+
11
14
  @implementation ANNSlotComponentView {
12
15
  // The React children live here. It waits hidden inside the slot, then moves into the container.
13
16
  ANNSlotContentView *_contentView;
14
17
  ANNSlotShadowNode::ConcreteState::Shared _state;
15
- BOOL _observing;
16
18
  // The layout that measures the content was asked for once.
17
19
  BOOL _measureAsked;
18
20
  }
@@ -33,19 +35,8 @@ using namespace facebook::react;
33
35
  return self;
34
36
  }
35
37
 
36
- - (void)dealloc {
37
- [NSNotificationCenter.defaultCenter removeObserver:self];
38
- }
39
-
40
38
  - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps {
41
39
  [super updateProps:props oldProps:oldProps];
42
- if (!_observing) {
43
- _observing = YES;
44
- [NSNotificationCenter.defaultCenter addObserver:self
45
- selector:@selector(containerDidChange:)
46
- name:ANNSlotContainerDidChangeNotification
47
- object:nil];
48
- }
49
40
  [self attachContent];
50
41
  }
51
42
 
@@ -54,13 +45,23 @@ using namespace facebook::react;
54
45
  [self attachContent];
55
46
  }
56
47
 
57
- - (void)containerDidChange:(NSNotification *)notification {
58
- if ([notification.object isEqual:self.nativeId]) {
48
+ #pragma mark - ANNSlotView
49
+
50
+ - (void)slotContainerDidChange:(NSString *)slotId {
51
+ if ([slotId isEqualToString:self.nativeId]) {
59
52
  [self attachContent];
60
53
  }
61
54
  }
62
55
 
56
+ #pragma mark - Content
57
+
63
58
  - (void)attachContent {
59
+ // The view hears the changes of the container under the slot id it has now, and no other.
60
+ if (self.nativeId != nil) {
61
+ [ANNSlotContainers registerSlotView:self forSlotId:self.nativeId];
62
+ } else {
63
+ [ANNSlotContainers unregisterSlotView:self];
64
+ }
64
65
  UIView *container = self.nativeId ? [ANNSlotContainers containerForSlotId:self.nativeId] : nil;
65
66
  UIView *target = container ?: self;
66
67
  if (_contentView.superview != target) {
@@ -108,15 +109,21 @@ using namespace facebook::react;
108
109
  return nil;
109
110
  }
110
111
 
111
- // A recycled slot waits for another screen: it stops listening and drops the old screen's state.
112
+ // A recycled slot waits for another screen: it leaves its slot id and drops the old screen's state. Its new props put
113
+ // it under the new id.
112
114
  - (void)prepareForRecycle {
113
115
  [super prepareForRecycle];
114
- [NSNotificationCenter.defaultCenter removeObserver:self];
115
- _observing = NO;
116
+ [ANNSlotContainers unregisterSlotView:self];
116
117
  _measureAsked = NO;
117
118
  _state.reset();
118
119
  [_contentView removeFromSuperview];
119
120
  _contentView.hidden = YES;
120
121
  }
121
122
 
123
+ // An unmounted slot that Fabric doesn't keep for reuse leaves its slot id too.
124
+ - (void)invalidate {
125
+ [super invalidate];
126
+ [ANNSlotContainers unregisterSlotView:self];
127
+ }
128
+
122
129
  @end
@@ -2,9 +2,6 @@
2
2
 
3
3
  NS_ASSUME_NONNULL_BEGIN
4
4
 
5
- /// Posted when the container of a slot appears or changes its size; the object is the slot id.
6
- extern NSNotificationName const ANNSlotContainerDidChangeNotification;
7
-
8
5
  /// A component named with this prefix is a screen of the app's one React tree: it has no React surface of its own,
9
6
  /// and the slot of the tree with its componentId shows it. Events and the state give its name without the prefix.
10
7
  extern NSString *const ANNSlotPrefix;
@@ -17,8 +14,14 @@ NSString *ANNComponentNameWithoutSlotPrefix(NSString *name);
17
14
  - (void)slotContentDidMount;
18
15
  @end
19
16
 
20
- /// The native containers the slots of the app's React tree show their children in, by slot id. A destroyed
21
- /// container drops out on its own.
17
+ /// A slot view of the app's React tree: it hears when the container of its slot id appears, goes or changes its size.
18
+ @protocol ANNSlotView <NSObject>
19
+ - (void)slotContainerDidChange:(NSString *)slotId;
20
+ @end
21
+
22
+ /// The native containers the slots of the app's React tree show their children in, by slot id, and the slot views by
23
+ /// the same id: a change of a container goes straight to the views of its id. A destroyed container and a deallocated
24
+ /// view drop out on their own. Main queue only.
22
25
  @interface ANNSlotContainers : NSObject
23
26
 
24
27
  + (void)setContainer:(UIView *)container forSlotId:(NSString *)slotId;
@@ -26,9 +29,14 @@ NSString *ANNComponentNameWithoutSlotPrefix(NSString *name);
26
29
  /// The container goes: its slot takes its content back.
27
30
  + (void)removeContainerForSlotId:(NSString *)slotId;
28
31
 
29
- /// The container of a slot appeared or changed its size.
32
+ /// The container of a slot appeared or changed its size: the slot views under its id, and only they, hear it.
30
33
  + (void)containerDidChange:(NSString *)slotId;
31
34
 
35
+ /// The slot view goes under its slot id, and out from under the id it had. Held weakly.
36
+ + (void)registerSlotView:(id<ANNSlotView>)view forSlotId:(NSString *)slotId;
37
+ /// The slot view unmounted or lost its slot id: no change of a container reaches it.
38
+ + (void)unregisterSlotView:(id<ANNSlotView>)view;
39
+
32
40
  @end
33
41
 
34
42
  NS_ASSUME_NONNULL_END
@@ -1,6 +1,6 @@
1
1
  #import "ANNSlotContainers.h"
2
+ #import "ANNAssert.h"
2
3
 
3
- NSNotificationName const ANNSlotContainerDidChangeNotification = @"ANNSlotContainerDidChange";
4
4
  NSString *const ANNSlotPrefix = @"slot:";
5
5
 
6
6
  NSString *ANNComponentNameWithoutSlotPrefix(NSString *name) {
@@ -32,8 +32,81 @@ NSString *ANNComponentNameWithoutSlotPrefix(NSString *name) {
32
32
  [self containerDidChange:slotId];
33
33
  }
34
34
 
35
+ // The slot views by slot id, weakly, in the order they came. Two live views under one id both stay and both hear
36
+ // the change, in that order, as every view with the id did when the change went to all of them.
37
+ + (NSMutableDictionary<NSString *, NSPointerArray *> *)slotViews {
38
+ static NSMutableDictionary *slotViews;
39
+ static dispatch_once_t once;
40
+ dispatch_once(&once, ^{
41
+ slotViews = [NSMutableDictionary dictionary];
42
+ });
43
+ return slotViews;
44
+ }
45
+
46
+ // The id each slot view is under, so a view is under one id at a time.
47
+ + (NSMapTable<id<ANNSlotView>, NSString *> *)slotIds {
48
+ static NSMapTable *slotIds;
49
+ static dispatch_once_t once;
50
+ dispatch_once(&once, ^{
51
+ slotIds = [[NSMapTable alloc]
52
+ initWithKeyOptions:NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality
53
+ valueOptions:NSPointerFunctionsStrongMemory
54
+ capacity:0];
55
+ });
56
+ return slotIds;
57
+ }
58
+
35
59
  + (void)containerDidChange:(NSString *)slotId {
36
- [NSNotificationCenter.defaultCenter postNotificationName:ANNSlotContainerDidChangeNotification object:slotId];
60
+ ANNAssertMainQueue();
61
+ // A copy: a view may register or leave while it hears the change.
62
+ for (id<ANNSlotView> view in [self slotViewsForSlotId:slotId]) {
63
+ [view slotContainerDidChange:slotId];
64
+ }
65
+ }
66
+
67
+ + (void)registerSlotView:(id<ANNSlotView>)view forSlotId:(NSString *)slotId {
68
+ ANNAssertMainQueue();
69
+ if ([[self.slotIds objectForKey:view] isEqualToString:slotId]) {
70
+ return;
71
+ }
72
+ [self unregisterSlotView:view];
73
+ [self.slotIds setObject:slotId forKey:view];
74
+ NSPointerArray *views = self.slotViews[slotId];
75
+ if (views == nil) {
76
+ views = [NSPointerArray weakObjectsPointerArray];
77
+ self.slotViews[slotId] = views;
78
+ }
79
+ [views addPointer:(__bridge void *)view];
80
+ }
81
+
82
+ + (void)unregisterSlotView:(id<ANNSlotView>)view {
83
+ ANNAssertMainQueue();
84
+ NSString *slotId = [self.slotIds objectForKey:view];
85
+ if (slotId == nil) {
86
+ return;
87
+ }
88
+ [self.slotIds removeObjectForKey:view];
89
+ [self pruneSlotId:slotId dropping:view];
90
+ }
91
+
92
+ + (NSArray<id<ANNSlotView>> *)slotViewsForSlotId:(NSString *)slotId {
93
+ [self pruneSlotId:slotId dropping:nil];
94
+ return self.slotViews[slotId].allObjects ?: @[];
95
+ }
96
+
97
+ // Drops the deallocated views under the id and `view`; an id with no views left goes. By hand rather than with
98
+ // `compact`, which keeps the nil entries of a weak pointer array.
99
+ + (void)pruneSlotId:(NSString *)slotId dropping:(nullable id<ANNSlotView>)view {
100
+ NSPointerArray *views = self.slotViews[slotId];
101
+ for (NSInteger index = (NSInteger)views.count - 1; index >= 0; index--) {
102
+ void *pointer = [views pointerAtIndex:(NSUInteger)index];
103
+ if (pointer == NULL || pointer == (__bridge void *)view) {
104
+ [views removePointerAtIndex:(NSUInteger)index];
105
+ }
106
+ }
107
+ if (views != nil && views.count == 0) {
108
+ [self.slotViews removeObjectForKey:slotId];
109
+ }
37
110
  }
38
111
 
39
112
  @end
@@ -17,7 +17,7 @@ typedef NS_ENUM(NSInteger, ANNBarButtonsSide) {
17
17
  /// merge of a side. Each button is an `ANNUIBarButtonItem` of the first kind its fields give (React view → icon and
18
18
  /// text → image button → SF Symbol → image → text → system item); a button without `id` is not placed. Side colors go
19
19
  /// onto what is built, never into the options; what the user chose (a toggle, a menu's check) goes into the screen's
20
- /// own options. The React buttons' slots are kept by component id, so a button built again reuses its slot. Main queue.
20
+ /// user layer. The React buttons' slots are kept by component id, so a button built again reuses its slot. Main queue.
21
21
  @interface ANNBarButtons : NSObject
22
22
 
23
23
  - (instancetype)initWithScreen:(UIViewController *)screen
@@ -49,12 +49,16 @@ typedef NS_ENUM(NSInteger, ANNBarButtonsSide) {
49
49
  /// The width the slot of a React button measured; 0 before it rendered.
50
50
  - (CGFloat)measuredWidthOf:(NSString *)componentId;
51
51
 
52
- /// A menu item `itemId` was picked: `ANN.NavigationButtonPressed` with its id, then the check of a single-selection
53
- /// menu written into the screen's own copy of the menu. `menuOf` gives the menu in `options` (a button's, the title's,
54
- /// the overflow menu); `setMenu` puts the copy into the own options.
52
+ /// A menu item `itemId` was picked: `ANN.NavigationButtonPressed` with its id, then, in a single-selection list, the
53
+ /// pick kept in the screen's user layer under `choiceKey`, one choice per menu. The resolution plays the picks again on
54
+ /// a copy of the menu it has. `menuOf` gives the menu in `options` (a button's, the title's, the overflow menu),
55
+ /// `setMenu` puts a changed copy there. A merge into the screen takes a pick away when the screen's resolution after it
56
+ /// has no such item, or when it gives the menu with other checks in the pick's level than the app had there, as on
57
+ /// Android. The two blocks capture no presenter: the node keeps them with the choice.
55
58
  - (void)menuItemPicked:(NSString *)itemId
59
+ choiceKey:(NSString *)choiceKey
56
60
  menuOf:(ANNMenuOptions *_Nullable (^)(ANNNavigationOptions *options))menuOf
57
- setMenu:(void (^)(ANNNavigationOptions *own, ANNMenuOptions *menu))setMenu;
61
+ setMenu:(void (^)(ANNNavigationOptions *options, ANNMenuOptions *menu))setMenu;
58
62
 
59
63
  /// A copy of `menu` that shares no item with it (a new `ANNMenuOptions` and a new `ANNMenuItemOptions` per item, at
60
64
  /// any depth).