@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,61 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /// Where an image of the options comes from.
6
+ typedef NS_ENUM(NSInteger, ANNImageSourceKind) {
7
+ /// An SF Symbol (`{ system, fallback? }`); a name the system doesn't have takes the fallback.
8
+ ANNImageSourceKindSymbol,
9
+ /// A `file:` address: a file, or an asset of the app's bundle that React Native resolved.
10
+ ANNImageSourceKindFile,
11
+ /// A `data:` address.
12
+ ANNImageSourceKindData,
13
+ /// Metro's asset in development, read from the dev server.
14
+ ANNImageSourceKindMetro,
15
+ };
16
+
17
+ /// An image the options name, as they name it: read when the options are read, loaded when an element shows it
18
+ /// (`ANNOptionImages`). Immutable; any thread.
19
+ @interface ANNImageSource : NSObject
20
+
21
+ - (instancetype)init NS_UNAVAILABLE;
22
+ + (instancetype)new NS_UNAVAILABLE;
23
+
24
+ /// An SF Symbol, with the source a name the system doesn't have takes.
25
+ + (instancetype)symbolNamed:(NSString *)name fallback:(nullable ANNImageSource *)fallback;
26
+
27
+ /// An address of `kind` (not a symbol) with the source's `scale`, `width` and `height` (0 without them).
28
+ + (instancetype)sourceOfKind:(ANNImageSourceKind)kind
29
+ url:(NSURL *)url
30
+ address:(NSString *)address
31
+ scale:(CGFloat)scale
32
+ width:(CGFloat)width
33
+ height:(CGFloat)height;
34
+
35
+ @property(nonatomic, readonly) ANNImageSourceKind kind;
36
+ @property(nonatomic, copy, readonly, nullable) NSString *symbolName;
37
+ @property(nonatomic, strong, readonly, nullable) ANNImageSource *fallback;
38
+ @property(nonatomic, strong, readonly, nullable) NSURL *url;
39
+ /// The address as the options gave it (with its bundle); the symbol's name for a symbol.
40
+ @property(nonatomic, copy, readonly) NSString *address;
41
+ @property(nonatomic, readonly) CGFloat scale;
42
+ @property(nonatomic, readonly) CGFloat width;
43
+ @property(nonatomic, readonly) CGFloat height;
44
+
45
+ /// What the decoded image depends on: the address, the scale and the width (the scale without one), and for a `file:`
46
+ /// source outside the app's bundle the file's modification date and size, read at each ask, so a file the app writes
47
+ /// again at the same path shows its new content. The cache's key.
48
+ @property(nonatomic, copy, readonly) NSString *key;
49
+
50
+ /// What the options name, as text: the kind, the address, the scale and the width, and a symbol's fallback, without the
51
+ /// file's stamp, so what the file holds now never changes it. Reads nothing from the disk. For telling whether two
52
+ /// options name the same image (`key` is for the decoded one).
53
+ @property(nonatomic, copy, readonly) NSString *sourceKey;
54
+
55
+ /// The image reads from the device alone (a file, an asset of the app, a `data:` address), so it may load off the main
56
+ /// queue; Metro's asset reads from the network and a symbol is the system's.
57
+ @property(nonatomic, readonly) BOOL loadsLater;
58
+
59
+ @end
60
+
61
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,101 @@
1
+ #import "ANNImageSource.h"
2
+
3
+ // The path of a `file:` source the app may write again: a file outside the app's bundle (documents, caches, a temporary
4
+ // folder); nil for any other source. The bundle's files and assets don't change while the app runs.
5
+ static NSString *ANNWritablePathOf(ANNImageSourceKind kind, NSURL *url) {
6
+ if (kind != ANNImageSourceKindFile || !url.isFileURL) {
7
+ return nil;
8
+ }
9
+ NSString *path = url.path;
10
+ NSString *bundle = NSBundle.mainBundle.bundlePath;
11
+ if (!path.isAbsolutePath || [path isEqualToString:bundle] ||
12
+ [path hasPrefix:[bundle stringByAppendingString:@"/"]]) {
13
+ return nil;
14
+ }
15
+ return path;
16
+ }
17
+
18
+ // The modification date and size of the file at `path` (else of its `.png`, as the loader looks for it), or nil.
19
+ static NSString *ANNFileStampOf(NSString *path) {
20
+ NSFileManager *files = NSFileManager.defaultManager;
21
+ NSDictionary<NSFileAttributeKey, id> *attributes = [files attributesOfItemAtPath:path
22
+ error:nil];
23
+ if (!attributes && path.pathExtension.length == 0) {
24
+ attributes = [files attributesOfItemAtPath:[path stringByAppendingPathExtension:@"png"]
25
+ error:nil];
26
+ }
27
+ NSDate *modified = attributes.fileModificationDate;
28
+ if (!modified) {
29
+ return nil;
30
+ }
31
+ return [NSString stringWithFormat:@"%.6f|%llu", modified.timeIntervalSinceReferenceDate,
32
+ attributes.fileSize];
33
+ }
34
+
35
+ @implementation ANNImageSource {
36
+ // The key without the file's stamp, and the path whose stamp joins it.
37
+ NSString *_baseKey;
38
+ NSString *_writablePath;
39
+ }
40
+
41
+ + (instancetype)symbolNamed:(NSString *)name fallback:(ANNImageSource *)fallback {
42
+ ANNImageSource *source = [[self alloc] initWithKind:ANNImageSourceKindSymbol
43
+ url:nil
44
+ address:name
45
+ scale:0
46
+ width:0
47
+ height:0];
48
+ source->_symbolName = [name copy];
49
+ source->_fallback = fallback;
50
+ return source;
51
+ }
52
+
53
+ + (instancetype)sourceOfKind:(ANNImageSourceKind)kind
54
+ url:(NSURL *)url
55
+ address:(NSString *)address
56
+ scale:(CGFloat)scale
57
+ width:(CGFloat)width
58
+ height:(CGFloat)height {
59
+ return [[self alloc] initWithKind:kind
60
+ url:url
61
+ address:address
62
+ scale:scale
63
+ width:width
64
+ height:height];
65
+ }
66
+
67
+ - (instancetype)initWithKind:(ANNImageSourceKind)kind
68
+ url:(NSURL *)url
69
+ address:(NSString *)address
70
+ scale:(CGFloat)scale
71
+ width:(CGFloat)width
72
+ height:(CGFloat)height {
73
+ if (self = [super init]) {
74
+ _kind = kind;
75
+ _url = url;
76
+ _address = [address copy];
77
+ _scale = scale;
78
+ _width = width;
79
+ _height = height;
80
+ _baseKey = [NSString stringWithFormat:@"%g|%g|%@", scale, width, _address];
81
+ _writablePath = ANNWritablePathOf(kind, url);
82
+ }
83
+ return self;
84
+ }
85
+
86
+ // Read at each ask: a file the app writes again at the same path is another image.
87
+ - (NSString *)key {
88
+ NSString *stamp = _writablePath ? ANNFileStampOf(_writablePath) : nil;
89
+ return stamp ? [NSString stringWithFormat:@"%@|%@", _baseKey, stamp] : _baseKey;
90
+ }
91
+
92
+ - (NSString *)sourceKey {
93
+ NSString *own = [NSString stringWithFormat:@"%ld|%@", (long)_kind, _baseKey];
94
+ return _fallback ? [NSString stringWithFormat:@"%@|%@", own, _fallback.sourceKey] : own;
95
+ }
96
+
97
+ - (BOOL)loadsLater {
98
+ return _kind == ANNImageSourceKindFile || _kind == ANNImageSourceKindData;
99
+ }
100
+
101
+ @end
@@ -0,0 +1,37 @@
1
+ #import "ANNImageSource.h"
2
+ #import <UIKit/UIKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ /// The images of the options, every one of them (bar buttons and their menus, the back button, tab icons, search
7
+ /// tokens and suggestions): one cache for the app, by the source's key (`NSCache`, its cost the image's bytes, let go
8
+ /// at a memory warning). The images don't depend on the JavaScript, so a reload keeps them.
9
+ ///
10
+ /// Every decode runs on one serial queue of the loader: `imageOf:` waits for it, `loadLater:done:` doesn't. A failure
11
+ /// (a missing file, data that isn't an image or doesn't decode, an exception while decoding) never throws: no image,
12
+ /// and one warning per address in the log. It is not cached, as on Android: it answers for a moment (1 s, the reads of
13
+ /// one apply), then the next read tries again, so a file written later or Metro's server back shows.
14
+ @interface ANNOptionImages : NSObject
15
+
16
+ - (instancetype)init NS_UNAVAILABLE;
17
+ + (instancetype)new NS_UNAVAILABLE;
18
+
19
+ /// The image of `source`: a symbol, what the cache has, or else loaded before this returns; nil on a failure. Any
20
+ /// thread.
21
+ + (nullable UIImage *)imageOf:(ANNImageSource *)source;
22
+
23
+ /// The cache doesn't have the image of `source`, it didn't fail a moment ago, and it loads off the main queue
24
+ /// (`loadsLater` of the source): what `loadLater:done:` doesn't give at once. Any thread.
25
+ + (BOOL)loadsLater:(ANNImageSource *)source;
26
+
27
+ /// Loads `source` on the loader's queue and gives `done` the image, or nil on a failure, on the main queue; one load
28
+ /// for every caller that asks for the same image meanwhile. What the cache has, or what doesn't load off the main
29
+ /// queue, comes at once, before this returns. Main queue.
30
+ + (void)loadLater:(ANNImageSource *)source done:(void (^)(UIImage *_Nullable image))done;
31
+
32
+ /// Lets every image of the cache go. Any thread.
33
+ + (void)clear;
34
+
35
+ @end
36
+
37
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,284 @@
1
+ #import "ANNOptionImages.h"
2
+ #import <React/RCTAssert.h>
3
+ #import <React/RCTLog.h>
4
+ #import <React/RCTUtils.h>
5
+
6
+ // The options' icons are small: 16 MB of decoded pixels holds hundreds of them.
7
+ static const NSUInteger ANNOptionImagesCostLimit = 16 * 1024 * 1024;
8
+
9
+ // The longest piece of an address a warning names (a `data:` address is the whole image).
10
+ static const NSUInteger ANNOptionImagesLoggedAddressLength = 80;
11
+
12
+ // How long a failure answers for its source without a new try: the many reads of one apply ask once, a later apply
13
+ // tries again (a file written since, Metro's server back).
14
+ static const NSTimeInterval ANNOptionImagesFailureMoment = 1;
15
+
16
+ static const void *ANNOptionImagesQueueKey = &ANNOptionImagesQueueKey;
17
+
18
+ static NSCache<NSString *, UIImage *> *ANNCache;
19
+ static dispatch_queue_t ANNQueue;
20
+ // The addresses already named in a warning: one each.
21
+ static NSMutableSet<NSString *> *ANNWarned;
22
+ // The keys whose last try failed, with the system's uptime then; guarded by itself. Failures never go into the cache.
23
+ static NSMutableDictionary<NSString *, NSNumber *> *ANNFailures;
24
+ // The loads off the main queue under way, with who waits for each. Main queue.
25
+ static NSMutableDictionary<NSString *, NSMutableArray *> *ANNLoading;
26
+
27
+ static void ANNSetUp(void) {
28
+ static dispatch_once_t once;
29
+ dispatch_once(&once, ^{
30
+ ANNCache = [NSCache new];
31
+ ANNCache.name = @"ApexNavigation option images";
32
+ ANNCache.totalCostLimit = ANNOptionImagesCostLimit;
33
+ dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class(
34
+ DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
35
+ ANNQueue = dispatch_queue_create("dev.apexnative.navigation.images", attributes);
36
+ dispatch_queue_set_specific(ANNQueue, ANNOptionImagesQueueKey,
37
+ (void *)ANNOptionImagesQueueKey, NULL);
38
+ ANNWarned = [NSMutableSet new];
39
+ ANNFailures = [NSMutableDictionary new];
40
+ ANNLoading = [NSMutableDictionary new];
41
+ NSNotificationCenter *center = NSNotificationCenter.defaultCenter;
42
+ [center addObserverForName:UIApplicationDidReceiveMemoryWarningNotification
43
+ object:nil
44
+ queue:nil
45
+ usingBlock:^(NSNotification *notification) {
46
+ [ANNCache removeAllObjects];
47
+ }];
48
+ });
49
+ }
50
+
51
+ // The bytes of the decoded image, the cost the cache weighs it with.
52
+ static NSUInteger ANNBytesOf(UIImage *image) {
53
+ CGImageRef cgImage = image.CGImage;
54
+ if (cgImage) {
55
+ return CGImageGetBytesPerRow(cgImage) * CGImageGetHeight(cgImage);
56
+ }
57
+ CGFloat width = image.size.width * image.scale;
58
+ CGFloat height = image.size.height * image.scale;
59
+ return (NSUInteger)MAX(1, width * height * 4);
60
+ }
61
+
62
+ static void ANNWarnOnce(ANNImageSource *source, NSString *why) {
63
+ @synchronized(ANNWarned) {
64
+ if ([ANNWarned containsObject:source.address]) {
65
+ return;
66
+ }
67
+ [ANNWarned addObject:source.address];
68
+ }
69
+ NSString *address = source.address;
70
+ if (address.length > ANNOptionImagesLoggedAddressLength) {
71
+ address = [[address substringToIndex:ANNOptionImagesLoggedAddressLength]
72
+ stringByAppendingString:@"…"];
73
+ }
74
+ RCTLogWarn(@"[ApexNavigation] could not load the image %@: %@", address, why);
75
+ }
76
+
77
+ // The last try of `key` failed a moment ago.
78
+ static BOOL ANNFailedJustNow(NSString *key) {
79
+ @synchronized(ANNFailures) {
80
+ NSNumber *at = ANNFailures[key];
81
+ if (!at) {
82
+ return NO;
83
+ }
84
+ NSTimeInterval since = NSProcessInfo.processInfo.systemUptime - at.doubleValue;
85
+ if (since < ANNOptionImagesFailureMoment) {
86
+ return YES;
87
+ }
88
+ [ANNFailures removeObjectForKey:key];
89
+ return NO;
90
+ }
91
+ }
92
+
93
+ static void ANNSetFailed(NSString *key, BOOL failed) {
94
+ @synchronized(ANNFailures) {
95
+ if (failed) {
96
+ ANNFailures[key] = @(NSProcessInfo.processInfo.systemUptime);
97
+ } else {
98
+ [ANNFailures removeObjectForKey:key];
99
+ }
100
+ }
101
+ }
102
+
103
+ // A file at the address: an image with nothing but its pixels, which can be decoded ahead. An asset of a catalog
104
+ // (found by name) may have a dark variant, which a decoded copy would lose.
105
+ static BOOL ANNIsFileAt(NSURL *url) {
106
+ NSString *path = url.path;
107
+ if (path.length == 0) {
108
+ return NO;
109
+ }
110
+ NSFileManager *files = NSFileManager.defaultManager;
111
+ if ([files fileExistsAtPath:path]) {
112
+ return YES;
113
+ }
114
+ return path.pathExtension.length == 0 &&
115
+ [files fileExistsAtPath:[path stringByAppendingPathExtension:@"png"]];
116
+ }
117
+
118
+ @implementation ANNOptionImages
119
+
120
+ + (UIImage *)imageOf:(ANNImageSource *)source {
121
+ if (source.kind == ANNImageSourceKindSymbol) {
122
+ UIImage *symbol = [UIImage systemImageNamed:source.symbolName];
123
+ return symbol ?: (source.fallback ? [self imageOf:source.fallback] : nil);
124
+ }
125
+ ANNSetUp();
126
+ NSString *key = source.key;
127
+ UIImage *cached = [ANNCache objectForKey:key];
128
+ if (cached || ANNFailedJustNow(key)) {
129
+ return cached;
130
+ }
131
+ if (dispatch_get_specific(ANNOptionImagesQueueKey)) {
132
+ return [self loadOnQueue:source];
133
+ }
134
+ __block UIImage *image = nil;
135
+ dispatch_sync(ANNQueue, ^{
136
+ image = [self loadOnQueue:source];
137
+ });
138
+ return image;
139
+ }
140
+
141
+ // A failure of a moment ago answers at once, so an item built again when its load failed doesn't load again.
142
+ + (BOOL)loadsLater:(ANNImageSource *)source {
143
+ if (!source.loadsLater) {
144
+ return NO;
145
+ }
146
+ ANNSetUp();
147
+ NSString *key = source.key;
148
+ return [ANNCache objectForKey:key] == nil && !ANNFailedJustNow(key);
149
+ }
150
+
151
+ + (void)loadLater:(ANNImageSource *)source done:(void (^)(UIImage *))done {
152
+ RCTAssertMainQueue();
153
+ if (![self loadsLater:source]) {
154
+ done([self imageOf:source]);
155
+ return;
156
+ }
157
+ NSString *key = source.key;
158
+ NSMutableArray *waiters = ANNLoading[key];
159
+ if (waiters) {
160
+ [waiters addObject:[done copy]];
161
+ return;
162
+ }
163
+ ANNLoading[key] = [NSMutableArray arrayWithObject:[done copy]];
164
+ dispatch_async(ANNQueue, ^{
165
+ UIImage *image = [self loadOnQueue:source];
166
+ dispatch_async(dispatch_get_main_queue(), ^{
167
+ NSArray *waiting = ANNLoading[key];
168
+ [ANNLoading removeObjectForKey:key];
169
+ for (void (^waiter)(UIImage *) in waiting) {
170
+ waiter(image);
171
+ }
172
+ });
173
+ });
174
+ }
175
+
176
+ + (void)clear {
177
+ ANNSetUp();
178
+ [ANNCache removeAllObjects];
179
+ @synchronized(ANNFailures) {
180
+ [ANNFailures removeAllObjects];
181
+ }
182
+ }
183
+
184
+ #pragma mark - The loader's queue
185
+
186
+ // The cache again (a load that came first), else the decode. An image goes into the cache; a failure doesn't, as on
187
+ // Android: it answers only for a moment (`ANNFailedJustNow`), and the next apply after that tries again.
188
+ + (UIImage *)loadOnQueue:(ANNImageSource *)source {
189
+ NSString *key = source.key;
190
+ UIImage *cached = [ANNCache objectForKey:key];
191
+ if (cached || ANNFailedJustNow(key)) {
192
+ return cached;
193
+ }
194
+ NSString *failure = nil;
195
+ UIImage *image = nil;
196
+ @try {
197
+ image = [self decode:source failure:&failure];
198
+ } @catch (NSException *exception) {
199
+ image = nil;
200
+ failure = [NSString
201
+ stringWithFormat:@"decoding it threw %@ (%@)", exception.name, exception.reason];
202
+ }
203
+ if (!image) {
204
+ ANNSetFailed(key, YES);
205
+ ANNWarnOnce(source, failure ?: @"it can't be decoded");
206
+ return nil;
207
+ }
208
+ ANNSetFailed(key, NO);
209
+ [ANNCache setObject:image forKey:key cost:ANNBytesOf(image)];
210
+ return image;
211
+ }
212
+
213
+ // The image of an address, as React Native's converter of images reads it, with the scale of the source; nil with
214
+ // what failed. A plain file and data are decoded here, so the first frame that shows them doesn't.
215
+ + (UIImage *)decode:(ANNImageSource *)source failure:(NSString **)failure {
216
+ NSURL *url = source.url;
217
+ UIImage *image = nil;
218
+ BOOL decodesAhead = YES;
219
+ switch (source.kind) {
220
+ case ANNImageSourceKindFile:
221
+ image = RCTImageFromLocalAssetURL(url) ?: RCTImageFromLocalBundleAssetURL(url);
222
+ if (!image) {
223
+ *failure = @"there is no image at that address";
224
+ return nil;
225
+ }
226
+ decodesAhead = ANNIsFileAt(url);
227
+ break;
228
+ case ANNImageSourceKindData:
229
+ case ANNImageSourceKindMetro: {
230
+ // Metro's assets in development: read from the dev server, as before the cache.
231
+ NSError *error = nil;
232
+ NSData *data = url ? [NSData dataWithContentsOfURL:url options:0 error:&error] : nil;
233
+ if (data.length == 0) {
234
+ *failure = error.localizedDescription ?: @"it has no data";
235
+ return nil;
236
+ }
237
+ image = [UIImage imageWithData:data];
238
+ if (!image) {
239
+ *failure = @"its data is not an image";
240
+ return nil;
241
+ }
242
+ break;
243
+ }
244
+ case ANNImageSourceKindSymbol:
245
+ return nil;
246
+ }
247
+ // An image UIKit can't prepare (16 bits a channel, CMYK) it still draws: it stays as it is then, decoded when it
248
+ // first shows.
249
+ UIImage *decoded = decodesAhead ? [image imageByPreparingForDisplay] : nil;
250
+ if (decoded) {
251
+ image = decoded.CGImage ? [UIImage imageWithCGImage:decoded.CGImage
252
+ scale:image.scale
253
+ orientation:image.imageOrientation]
254
+ : decoded;
255
+ }
256
+ if (!(image.size.width > 0) || !(image.size.height > 0)) {
257
+ *failure = @"it is empty";
258
+ return nil;
259
+ }
260
+ return [self image:image scaledBy:source];
261
+ }
262
+
263
+ // The scale of the source; without it the pixel width over `width`; without both, the file's own. A size that
264
+ // doesn't match is only logged.
265
+ + (UIImage *)image:(UIImage *)image scaledBy:(ANNImageSource *)source {
266
+ CGFloat scale = source.scale;
267
+ CGFloat width = source.width;
268
+ CGFloat height = source.height;
269
+ if (scale <= 0 && width > 0 && image.CGImage) {
270
+ scale = CGImageGetWidth(image.CGImage) / width;
271
+ }
272
+ if (scale > 0 && image.CGImage && scale != image.scale) {
273
+ image = [UIImage imageWithCGImage:image.CGImage
274
+ scale:scale
275
+ orientation:image.imageOrientation];
276
+ }
277
+ if (width > 0 && height > 0 && !CGSizeEqualToSize(image.size, CGSizeMake(width, height))) {
278
+ RCTLogInfo(@"[ApexNavigation] an image of the options is %@, its source says %@",
279
+ NSStringFromCGSize(image.size), NSStringFromCGSize(CGSizeMake(width, height)));
280
+ }
281
+ return image;
282
+ }
283
+
284
+ @end
@@ -1,15 +1,24 @@
1
+ #import "ANNImageSource.h"
1
2
  #import "Param.h"
2
3
  #import <UIKit/UIKit.h>
3
4
 
4
5
  NS_ASSUME_NONNULL_BEGIN
5
6
 
6
- /// An image of the options, loaded when the options were read. An image that didn't load is no value: the element shows
7
- /// without it.
7
+ /// An image of the options: its source, read with the options, and the image, loaded when an element asks for it
8
+ /// through the shared cache (`ANNOptionImages`). A source that doesn't load is no image: the element shows without it.
9
+ /// A value that is a `UIImage` is that image, with no source: an element made again from an image that came already
10
+ /// (a tab's icon loaded off the main queue) doesn't ask the cache again.
8
11
  @interface Image : Param
9
12
 
13
+ /// The image, loaded at once when the cache doesn't have it; nil without a source and when it doesn't load.
10
14
  - (nullable UIImage *)get;
15
+ /// The image, or `defaultValue` without a source and when it doesn't load. `hasValue` says only that the options name
16
+ /// an image: a choice between an image and what shows without one reads the image itself.
11
17
  - (nullable UIImage *)withDefault:(nullable UIImage *)defaultValue;
12
18
 
19
+ /// What the options name; nil without a value.
20
+ @property(nonatomic, strong, readonly, nullable) ANNImageSource *source;
21
+
13
22
  @end
14
23
 
15
24
  NS_ASSUME_NONNULL_END
@@ -1,13 +1,26 @@
1
1
  #import "Image.h"
2
+ #import "ANNOptionImages.h"
2
3
 
3
4
  @implementation Image
4
5
 
6
+ - (ANNImageSource *)source {
7
+ id value = self.value;
8
+ return [value isKindOfClass:ANNImageSource.class] ? value : nil;
9
+ }
10
+
5
11
  - (UIImage *)get {
6
- return [super get];
12
+ id value = self.value;
13
+ if ([value isKindOfClass:UIImage.class]) {
14
+ return value;
15
+ }
16
+ ANNImageSource *source = self.source;
17
+ return source ? [ANNOptionImages imageOf:source] : nil;
7
18
  }
8
19
 
20
+ // A source that doesn't load is no image: the default then, as without a value.
9
21
  - (UIImage *)withDefault:(UIImage *)defaultValue {
10
- return [super withDefault:defaultValue];
22
+ UIImage *image = self.hasValue ? self.get : nil;
23
+ return image ?: defaultValue;
11
24
  }
12
25
 
13
26
  @end
@@ -2,11 +2,11 @@
2
2
 
3
3
  NS_ASSUME_NONNULL_BEGIN
4
4
 
5
- /// Reads `key` of the options' JSON as an image and loads it at once, on the main queue: `{ system, fallback? }` is an
6
- /// SF Symbol, else the fallback source; a source (a string, or an object with `uri`, `bundle`, `scale`, `width`,
7
- /// `height` and Metro's asset flag) loads a file, an asset of the app's bundle, a `data:` address, or in development a
8
- /// Metro asset. Remote addresses and arrays of sources are not loaded. A failure is no image and an error in the React
9
- /// Native log; it never throws.
5
+ /// Reads `key` of the options' JSON as the source of an image, which loads when an element shows it (`Image`):
6
+ /// `{ system, fallback? }` is an SF Symbol, else the fallback source; a source (a string, or an object with `uri`,
7
+ /// `bundle`, `scale`, `width`, `height` and Metro's asset flag) is a file, an asset of the app's bundle, a `data:`
8
+ /// address, or in development a Metro asset. Remote addresses and arrays of sources are no image, with an error in the
9
+ /// React Native log; it never throws.
10
10
  @interface ImageParser : NSObject
11
11
 
12
12
  + (Image *)parse:(nullable NSDictionary *)json key:(NSString *)key;