@apexnative/navigation 1.0.0-alpha.6 → 1.0.0-alpha.7

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 (236) hide show
  1. package/ApexNavigation.podspec +2 -1
  2. package/CHANGELOG.md +7 -0
  3. package/android/CLAUDE.md +19 -10
  4. package/android/CORE.md +3 -2
  5. package/android/consumer-rules.pro +6 -0
  6. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabOptions.kt +1 -1
  7. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +1 -1
  8. package/android/src/main/java/dev/apexnative/navigation/options/LargeTitleOptions.kt +1 -1
  9. package/android/src/main/java/dev/apexnative/navigation/options/LayoutAnimation.kt +1 -1
  10. package/android/src/main/java/dev/apexnative/navigation/options/OptionKey.kt +18 -0
  11. package/android/src/main/java/dev/apexnative/navigation/options/OptionPaths.kt +91 -0
  12. package/android/src/main/java/dev/apexnative/navigation/options/SheetOptions.kt +7 -4
  13. package/android/src/main/java/dev/apexnative/navigation/options/TitleOptions.kt +2 -2
  14. package/android/src/main/java/dev/apexnative/navigation/options/TopBarAppearanceOptions.kt +1 -1
  15. package/android/src/main/java/dev/apexnative/navigation/options/TopBarOptions.kt +16 -7
  16. package/android/src/main/java/dev/apexnative/navigation/react/ContentHeights.kt +95 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/ContentMounts.kt +66 -0
  18. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +4 -0
  19. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +41 -3
  20. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +16 -1
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +27 -7
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +12 -1
  23. package/android/src/main/java/dev/apexnative/navigation/utils/MaterialTheme.kt +5 -6
  24. package/android/src/main/java/dev/apexnative/navigation/utils/SystemAnimations.kt +36 -0
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +329 -55
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +26 -5
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +60 -2
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -2
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +32 -4
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +46 -7
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +15 -3
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +7 -4
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabMotion.kt +142 -0
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +215 -43
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +25 -26
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarRowTransition.kt +3 -2
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/ButtonPlaces.kt +11 -5
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +36 -38
  39. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +124 -181
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +8 -1
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionLayer.kt +37 -112
  42. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +56 -3
  43. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +170 -153
  44. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMorph.kt +199 -0
  45. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +55 -45
  46. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/{TextChangeAnimator.kt → TextMorph.kt} +79 -129
  47. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewMorph.kt +108 -0
  48. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabCopy.kt +85 -0
  49. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabMenu.kt +13 -0
  50. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +5 -24
  51. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +51 -34
  52. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonComponentCell.kt +6 -6
  53. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonView.kt +113 -0
  54. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarMenu.kt +219 -0
  55. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +121 -175
  56. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonMenus.kt +34 -64
  57. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonRow.kt +216 -0
  58. package/android/src/main/jni/ANNContentHeights.cpp +82 -0
  59. package/android/src/main/res/drawable/apex_navigation_ic_arrow_right.xml +14 -0
  60. package/android/src/main/res/layout/apex_navigation_menu_header.xml +19 -0
  61. package/android/src/main/res/layout/apex_navigation_menu_item.xml +68 -0
  62. package/android/src/main/res/values/styles.xml +1 -1
  63. package/android/src/main/res/values-night/styles.xml +1 -1
  64. package/cli/doctor/checks/app.js +16 -1
  65. package/cli/project.js +4 -0
  66. package/cli/setup/index.js +5 -0
  67. package/cli/setup/steps/metro.js +46 -0
  68. package/docs-md/architecture/native-screens.md +4 -0
  69. package/docs-md/architecture/theme-and-context.md +131 -12
  70. package/docs-md/architecture/transitions.md +43 -4
  71. package/docs-md/flows/bottom-sheets.md +4 -2
  72. package/docs-md/fundamentals/screens-and-props.md +9 -1
  73. package/docs-md/guides/cli.md +3 -1
  74. package/docs-md/help/troubleshooting.md +1 -1
  75. package/docs-md/index.json +29 -1
  76. package/docs-md/screen-ui/back-and-gestures.md +3 -1
  77. package/docs-md/screen-ui/bar-buttons.md +1 -1
  78. package/docs-md/screen-ui/toolbar-and-fab.md +2 -0
  79. package/docs-md/start/installation.md +16 -1
  80. package/ios/Animations/ANNFrameDriver.h +55 -15
  81. package/ios/Animations/ANNFrameDriver.mm +214 -60
  82. package/ios/Animations/ANNTransitionAnimator.h +26 -5
  83. package/ios/Animations/ANNTransitionAnimator.mm +49 -3
  84. package/ios/Animations/ANNTransitionPlan.h +2 -2
  85. package/ios/Animations/ANNTransitionPlan.mm +10 -0
  86. package/ios/Animations/Elements/ANNElementFinder.h +2 -2
  87. package/ios/Animations/Elements/ANNElementFinder.mm +2 -2
  88. package/ios/Animations/Elements/ANNElementTransition.h +3 -3
  89. package/ios/Animations/Elements/ANNElementTransition.mm +2 -2
  90. package/ios/Animations/Elements/ANNSharedElementFlight.h +3 -3
  91. package/ios/Animations/Elements/ANNSharedElementFlight.mm +181 -43
  92. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -5
  93. package/ios/Animations/Interpolators/ANNViewTracks.mm +51 -21
  94. package/ios/CLAUDE.md +15 -7
  95. package/ios/CORE.md +1 -1
  96. package/ios/Core/ANNAppDelegate.mm +0 -2
  97. package/ios/Core/ANNDeepLinkQueue.h +6 -5
  98. package/ios/Core/ANNDeepLinkQueue.mm +22 -10
  99. package/ios/Core/ANNSceneDelegate.mm +2 -2
  100. package/ios/Core/ApexNavigation.mm +6 -2
  101. package/ios/Layouts/Root/ANNRootCoordinator.mm +45 -15
  102. package/ios/Layouts/Screen/ANNComponentViewController.mm +4 -1
  103. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.h +11 -2
  104. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.mm +14 -7
  105. package/ios/Layouts/Stack/ANNStackController.h +8 -0
  106. package/ios/Layouts/Stack/ANNStackController.mm +153 -11
  107. package/ios/Layouts/Stack/ANNSwipePopInteraction.h +26 -0
  108. package/ios/Layouts/Stack/ANNSwipePopInteraction.mm +185 -0
  109. package/ios/Layouts/Stack/InteractivePopGestureDelegate.mm +28 -4
  110. package/ios/Options/ANNAnimationOptions.h +2 -1
  111. package/ios/Options/ANNAnimationOptions.mm +1 -1
  112. package/ios/Options/ANNOptions.h +7 -0
  113. package/ios/Options/ANNOptions.mm +45 -0
  114. package/ios/Options/ANNOptionsReader.mm +2 -4
  115. package/ios/Options/ANNStackPopAnimationOptions.h +13 -0
  116. package/ios/Options/ANNStackPopAnimationOptions.mm +11 -0
  117. package/ios/Options/Params/ColorParser.h +4 -3
  118. package/ios/Options/Params/ColorParser.mm +22 -4
  119. package/ios/React/ANNSurfaceHostingView.h +8 -0
  120. package/ios/React/ANNSurfaceHostingView.mm +28 -1
  121. package/ios/React/Slots/ANNSlotScreenView.mm +8 -2
  122. package/ios/TurboModules/ANNTurboModule.mm +46 -0
  123. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.cpp +20 -0
  124. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.h +5 -14
  125. package/lib/module/ARCHITECTURE.md +9 -3
  126. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  127. package/lib/module/api/app/AppRegistry.js +14 -2
  128. package/lib/module/api/app/AppRegistry.js.map +1 -1
  129. package/lib/module/api/app/createNavigation.js +188 -66
  130. package/lib/module/api/app/createNavigation.js.map +1 -1
  131. package/lib/module/api/events/navigationEvents.js +6 -2
  132. package/lib/module/api/events/navigationEvents.js.map +1 -1
  133. package/lib/module/api/linking/linking.js +11 -0
  134. package/lib/module/api/linking/linking.js.map +1 -1
  135. package/lib/module/api/screens/defineScreen.js +45 -3
  136. package/lib/module/api/screens/defineScreen.js.map +1 -1
  137. package/lib/module/api/screens/nativeScreens.js +31 -6
  138. package/lib/module/api/screens/nativeScreens.js.map +1 -1
  139. package/lib/module/api/screens/registerScreens.js +172 -35
  140. package/lib/module/api/screens/registerScreens.js.map +1 -1
  141. package/lib/module/api/theme/theme.js +5 -0
  142. package/lib/module/api/theme/theme.js.map +1 -1
  143. package/lib/module/engine/commands.js +21 -1
  144. package/lib/module/engine/commands.js.map +1 -1
  145. package/lib/module/engine/layout.js +7 -2
  146. package/lib/module/engine/layout.js.map +1 -1
  147. package/lib/module/engine/nativeScreens.js +9 -0
  148. package/lib/module/engine/nativeScreens.js.map +1 -1
  149. package/lib/module/engine/options.js +5 -4
  150. package/lib/module/engine/options.js.map +1 -1
  151. package/lib/module/engine/optionsChange.js +102 -0
  152. package/lib/module/engine/optionsChange.js.map +1 -0
  153. package/lib/module/engine/refresh.js +85 -0
  154. package/lib/module/engine/refresh.js.map +1 -0
  155. package/lib/module/engine/rememberedOptions.js +60 -0
  156. package/lib/module/engine/rememberedOptions.js.map +1 -1
  157. package/lib/module/engine/screens.js +26 -0
  158. package/lib/module/engine/screens.js.map +1 -1
  159. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +7 -0
  160. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  161. package/lib/typescript/api/app/AppRegistry.d.ts.map +1 -1
  162. package/lib/typescript/api/app/createNavigation.d.ts +5 -0
  163. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  164. package/lib/typescript/api/events/navigationEvents.d.ts +2 -2
  165. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  166. package/lib/typescript/api/index.d.ts +1 -1
  167. package/lib/typescript/api/index.d.ts.map +1 -1
  168. package/lib/typescript/api/linking/linking.d.ts +8 -0
  169. package/lib/typescript/api/linking/linking.d.ts.map +1 -1
  170. package/lib/typescript/api/screens/defineScreen.d.ts +23 -4
  171. package/lib/typescript/api/screens/defineScreen.d.ts.map +1 -1
  172. package/lib/typescript/api/screens/nativeScreens.d.ts +8 -1
  173. package/lib/typescript/api/screens/nativeScreens.d.ts.map +1 -1
  174. package/lib/typescript/api/screens/registerScreens.d.ts.map +1 -1
  175. package/lib/typescript/api/theme/theme.d.ts +2 -0
  176. package/lib/typescript/api/theme/theme.d.ts.map +1 -1
  177. package/lib/typescript/engine/commands.d.ts +5 -0
  178. package/lib/typescript/engine/commands.d.ts.map +1 -1
  179. package/lib/typescript/engine/layout.d.ts.map +1 -1
  180. package/lib/typescript/engine/nativeScreens.d.ts +5 -0
  181. package/lib/typescript/engine/nativeScreens.d.ts.map +1 -1
  182. package/lib/typescript/engine/optionsChange.d.ts +28 -0
  183. package/lib/typescript/engine/optionsChange.d.ts.map +1 -0
  184. package/lib/typescript/engine/refresh.d.ts +35 -0
  185. package/lib/typescript/engine/refresh.d.ts.map +1 -0
  186. package/lib/typescript/engine/rememberedOptions.d.ts +13 -0
  187. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  188. package/lib/typescript/engine/screens.d.ts +7 -0
  189. package/lib/typescript/engine/screens.d.ts.map +1 -1
  190. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  191. package/lib/typescript/interfaces/options/animations.d.ts +40 -15
  192. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  193. package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -2
  194. package/lib/typescript/interfaces/options/buttons.d.ts +2 -2
  195. package/lib/typescript/interfaces/options/modals.d.ts +6 -7
  196. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  197. package/lib/typescript/interfaces/options/stack.d.ts +8 -3
  198. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  199. package/lib/typescript/interfaces/options/topBar.d.ts +14 -0
  200. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  201. package/metro/acceptPlugin.js +57 -0
  202. package/metro/babelTransformer.js +45 -0
  203. package/metro/index.js +36 -0
  204. package/package.json +3 -1
  205. package/src/ARCHITECTURE.md +9 -3
  206. package/src/adapters/NativeANNTurboModule.ts +7 -0
  207. package/src/api/app/AppRegistry.ts +16 -2
  208. package/src/api/app/createNavigation.ts +198 -66
  209. package/src/api/events/navigationEvents.ts +9 -3
  210. package/src/api/index.ts +1 -0
  211. package/src/api/linking/linking.ts +22 -1
  212. package/src/api/screens/defineScreen.ts +49 -5
  213. package/src/api/screens/nativeScreens.ts +40 -4
  214. package/src/api/screens/registerScreens.tsx +170 -42
  215. package/src/api/theme/theme.ts +5 -0
  216. package/src/engine/commands.ts +21 -0
  217. package/src/engine/layout.ts +11 -2
  218. package/src/engine/nativeScreens.ts +10 -1
  219. package/src/engine/options.ts +6 -8
  220. package/src/engine/optionsChange.ts +107 -0
  221. package/src/engine/refresh.ts +97 -0
  222. package/src/engine/rememberedOptions.ts +70 -0
  223. package/src/engine/screens.tsx +24 -0
  224. package/src/interfaces/ComponentEvents.ts +2 -2
  225. package/src/interfaces/options/animations.ts +40 -15
  226. package/src/interfaces/options/bottomTabs.ts +2 -2
  227. package/src/interfaces/options/buttons.ts +2 -2
  228. package/src/interfaces/options/modals.ts +6 -7
  229. package/src/interfaces/options/stack.ts +8 -3
  230. package/src/interfaces/options/topBar.ts +14 -0
  231. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStub.kt +0 -31
  232. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +0 -134
  233. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +0 -78
  234. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PropertyAnimatorCreator.kt +0 -33
  235. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +0 -57
  236. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +0 -146
@@ -29,6 +29,21 @@ typedef struct {
29
29
  BOOL clipped;
30
30
  } ANNFlightEnd;
31
31
 
32
+ // What the copy shows at one time: its size, center, transform and corners, its two pictures' frames and alphas, and
33
+ // its visible part (in the copy's coordinates) with its corners.
34
+ typedef struct {
35
+ CGSize size;
36
+ CGPoint center;
37
+ CATransform3D transform;
38
+ CGFloat cornerRadius;
39
+ CGRect fromFrame;
40
+ CGRect toFrame;
41
+ CGFloat fromAlpha;
42
+ CGFloat toAlpha;
43
+ CGRect mask;
44
+ CGFloat maskRadius;
45
+ } ANNFlightState;
46
+
32
47
  static BOOL ANNIsText(UIView *view) {
33
48
  static Class paragraph;
34
49
  static dispatch_once_t once;
@@ -200,6 +215,30 @@ static UIImage *ANNPictureOf(UIView *view) {
200
215
  }];
201
216
  }
202
217
 
218
+ // A layer at `frame` in its superlayer: its bounds and its position (its anchor is its center).
219
+ static void ANNPlaceLayer(CALayer *layer, CGRect frame) {
220
+ layer.bounds = CGRectMake(0, 0, frame.size.width, frame.size.height);
221
+ layer.position = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
222
+ }
223
+
224
+ // The core's animation of `keyPath` on `layer` (`ANNKeyframes`), under its key.
225
+ static void ANNAddKeyframes(CALayer *layer, NSString *keyPath, NSArray *values,
226
+ NSArray<NSNumber *> *times, NSTimeInterval length) {
227
+ CAKeyframeAnimation *animation = ANNKeyframes(keyPath, values, times, length);
228
+ if (animation) {
229
+ [layer addAnimation:animation forKey:ANNAnimationKey(keyPath)];
230
+ }
231
+ }
232
+
233
+ // The bounds and the position of a layer at `frame`, as values of its animations.
234
+ static NSValue *ANNBoundsValue(CGRect frame) {
235
+ return [NSValue valueWithCGRect:CGRectMake(0, 0, frame.size.width, frame.size.height)];
236
+ }
237
+
238
+ static NSValue *ANNPositionValue(CGRect frame) {
239
+ return [NSValue valueWithCGPoint:CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame))];
240
+ }
241
+
203
242
  @implementation ANNSharedElementFlight {
204
243
  __weak UIView *_fromView;
205
244
  __weak UIView *_toView;
@@ -208,7 +247,9 @@ static UIImage *ANNPictureOf(UIView *view) {
208
247
  UIView *_copy;
209
248
  UIImageView *_fromPicture;
210
249
  UIImageView *_toPicture;
211
- CAShapeLayer *_mask;
250
+ // The visible part of the copy: a plain layer with corners, as Core Animation can't animate a path whose corners
251
+ // change.
252
+ CALayer *_mask;
212
253
  ANNFlightEnd _from;
213
254
  ANNFlightEnd _to;
214
255
  ANNTrack *_track;
@@ -343,7 +384,8 @@ static UIImage *ANNPictureOf(UIView *view) {
343
384
  // A text's pictures are neither clipped nor masked.
344
385
  _copy.clipsToBounds = !_text;
345
386
  if (!_text && (_from.clipped || _to.clipped)) {
346
- _mask = [CAShapeLayer layer];
387
+ _mask = [CALayer layer];
388
+ _mask.backgroundColor = UIColor.blackColor.CGColor;
347
389
  _copy.layer.mask = _mask;
348
390
  }
349
391
  [_copy addSubview:_fromPicture];
@@ -360,72 +402,168 @@ static UIImage *ANNPictureOf(UIView *view) {
360
402
  [container addSubview:_copy];
361
403
  from.hidden = YES;
362
404
  to.hidden = YES;
363
- [self applyAtTime:0];
405
+ [self placeAtTime:0];
364
406
  }
365
407
 
366
408
  - (UIView *)copyView {
367
409
  return _copy;
368
410
  }
369
411
 
370
- #pragma mark - ANNFrameClient
371
-
372
- - (NSTimeInterval)endTime {
373
- return _track.endTime;
374
- }
375
-
376
- - (void)applyAtTime:(NSTimeInterval)time {
377
- if (_restored) {
378
- return;
379
- }
412
+ // The copy's whole state at `time`: its own geometry, its pictures' and its visible part's.
413
+ - (ANNFlightState)stateAtTime:(NSTimeInterval)time {
414
+ ANNFlightState state;
380
415
  double f = [_track fractionAt:time];
381
- CGSize size = ANNInterpolateSize(_from.size, _to.size, f);
382
- _copy.layer.transform = CATransform3DIdentity;
383
- _copy.bounds = CGRectMake(0, 0, size.width, size.height);
384
- _copy.center = ANNInterpolatePoint(_from.center, _to.center, f);
385
- _copy.layer.transform =
386
- ANNTransformOfParts(ANNInterpolateTransformParts(_from.parts, _to.parts, f));
416
+ state.size = ANNInterpolateSize(_from.size, _to.size, f);
417
+ state.center = ANNInterpolatePoint(_from.center, _to.center, f);
418
+ state.transform = ANNTransformOfParts(ANNInterpolateTransformParts(_from.parts, _to.parts, f));
419
+ state.cornerRadius = 0;
420
+ state.mask = CGRectZero;
421
+ state.maskRadius = 0;
387
422
  if (_text) {
388
- [self placeTextPicturesAtTime:time size:size];
389
- return;
423
+ [self placeTextPicturesOf:&state atTime:time];
424
+ return state;
390
425
  }
391
- _copy.layer.cornerRadius = ANNInterpolateNumber(_from.cornerRadius, _to.cornerRadius, f);
392
- _fromPicture.frame = _copy.bounds;
393
- _toPicture.frame = _copy.bounds;
394
- _fromPicture.alpha = 1;
395
- _toPicture.alpha = (CGFloat)f;
426
+ state.cornerRadius = ANNInterpolateNumber(_from.cornerRadius, _to.cornerRadius, f);
427
+ CGRect whole = CGRectMake(0, 0, state.size.width, state.size.height);
428
+ state.fromFrame = whole;
429
+ state.toFrame = whole;
430
+ state.fromAlpha = 1;
431
+ state.toAlpha = (CGFloat)f;
396
432
  if (_mask) {
397
433
  CGRect fromWhole = CGRectMake(0, 0, _from.size.width, _from.size.height);
398
434
  CGRect toWhole = CGRectMake(0, 0, _to.size.width, _to.size.height);
399
435
  CGRect fromVisible = _from.clipped ? _from.visible : fromWhole;
400
436
  CGRect toVisible = _to.clipped ? _to.visible : toWhole;
401
- CGRect visible = ANNInterpolateRect(fromVisible, toVisible, f);
437
+ state.mask = ANNInterpolateRect(fromVisible, toVisible, f);
438
+ // At most half the shorter side, as a rounded path has it.
402
439
  CGFloat radius = ANNInterpolateNumber(_from.visibleRadius, _to.visibleRadius, f);
403
- _mask.frame = _copy.bounds;
404
- _mask.path = [UIBezierPath bezierPathWithRoundedRect:visible cornerRadius:radius].CGPath;
440
+ CGFloat half = MIN(state.mask.size.width, state.mask.size.height) / 2;
441
+ state.maskRadius = MAX(MIN(radius, half), 0);
405
442
  }
443
+ return state;
406
444
  }
407
445
 
408
446
  // The text's pictures keep their aspect at the height of the copy, at the line's start; they swap by the clock,
409
447
  // linearly, in the first quarter of the flight when the text grows, in the last when it shrinks.
410
- - (void)placeTextPicturesAtTime:(NSTimeInterval)time size:(CGSize)size {
411
- for (UIImageView *picture in @[ _fromPicture, _toPicture ]) {
412
- CGSize imageSize = picture.image.size;
413
- CGFloat height = size.height;
448
+ - (void)placeTextPicturesOf:(ANNFlightState *)state atTime:(NSTimeInterval)time {
449
+ CGRect frames[2];
450
+ UIImageView *pictures[2] = {_fromPicture, _toPicture};
451
+ for (int index = 0; index < 2; index++) {
452
+ CGSize imageSize = pictures[index].image.size;
453
+ CGFloat height = state->size.height;
414
454
  CGFloat width = imageSize.height > 0 ? imageSize.width * height / imageSize.height : 0;
415
- CGFloat x = _rightToLeft ? size.width - width : 0;
416
- picture.frame = CGRectMake(x, 0, width, height);
455
+ CGFloat x = _rightToLeft ? state->size.width - width : 0;
456
+ frames[index] = CGRectMake(x, 0, width, height);
417
457
  }
458
+ state->fromFrame = frames[0];
459
+ state->toFrame = frames[1];
460
+ double swap = [self swapAtTime:time];
461
+ state->fromAlpha = (CGFloat)(1 - swap);
462
+ state->toAlpha = (CGFloat)swap;
463
+ }
464
+
465
+ // The quarter of the flight the text's pictures swap in.
466
+ - (NSTimeInterval)swapStart {
418
467
  NSTimeInterval duration = _track.duration;
419
- NSTimeInterval quarter = duration / 4;
420
- NSTimeInterval start = _track.delay + (_grows ? 0 : duration - quarter);
421
- double swap;
468
+ return _track.delay + (_grows ? 0 : duration - duration / 4);
469
+ }
470
+
471
+ - (double)swapAtTime:(NSTimeInterval)time {
472
+ NSTimeInterval quarter = _track.duration / 4;
422
473
  if (quarter <= 0) {
423
- swap = time >= _track.delay ? 1 : 0;
424
- } else {
425
- swap = MIN(MAX((time - start) / quarter, 0.0), 1.0);
474
+ return time >= _track.delay ? 1 : 0;
475
+ }
476
+ return MIN(MAX((time - [self swapStart]) / quarter, 0.0), 1.0);
477
+ }
478
+
479
+ // The copy shows its state at `time` (its own values, with no animation).
480
+ - (void)placeAtTime:(NSTimeInterval)time {
481
+ ANNFlightState state = [self stateAtTime:time];
482
+ [CATransaction begin];
483
+ [CATransaction setDisableActions:YES];
484
+ _copy.layer.transform = CATransform3DIdentity;
485
+ _copy.bounds = CGRectMake(0, 0, state.size.width, state.size.height);
486
+ _copy.center = state.center;
487
+ _copy.layer.transform = state.transform;
488
+ if (!_text) {
489
+ _copy.layer.cornerRadius = state.cornerRadius;
490
+ }
491
+ ANNPlaceLayer(_fromPicture.layer, state.fromFrame);
492
+ ANNPlaceLayer(_toPicture.layer, state.toFrame);
493
+ _fromPicture.alpha = state.fromAlpha;
494
+ _toPicture.alpha = state.toAlpha;
495
+ if (_mask) {
496
+ ANNPlaceLayer(_mask, state.mask);
497
+ _mask.cornerRadius = state.maskRadius;
498
+ }
499
+ [CATransaction commit];
500
+ }
501
+
502
+ #pragma mark - ANNFrameClient
503
+
504
+ - (NSTimeInterval)endTime {
505
+ return _track.endTime;
506
+ }
507
+
508
+ - (void)addAnimationsWithLength:(NSTimeInterval)length {
509
+ if (_restored) {
510
+ return;
511
+ }
512
+ NSMutableArray<NSNumber *> *marks =
513
+ [NSMutableArray arrayWithObjects:@(_track.delay), @(_track.endTime), nil];
514
+ if (_text) {
515
+ [marks addObject:@([self swapStart])];
516
+ [marks addObject:@([self swapStart] + _track.duration / 4)];
517
+ }
518
+ NSArray<NSNumber *> *times = ANNSampleTimes(_track.endTime, length, marks);
519
+ NSMutableArray *bounds = [NSMutableArray new];
520
+ NSMutableArray *positions = [NSMutableArray new];
521
+ NSMutableArray *transforms = [NSMutableArray new];
522
+ NSMutableArray *radii = [NSMutableArray new];
523
+ NSMutableArray *fromBounds = [NSMutableArray new];
524
+ NSMutableArray *fromPositions = [NSMutableArray new];
525
+ NSMutableArray *fromAlphas = [NSMutableArray new];
526
+ NSMutableArray *toBounds = [NSMutableArray new];
527
+ NSMutableArray *toPositions = [NSMutableArray new];
528
+ NSMutableArray *toAlphas = [NSMutableArray new];
529
+ NSMutableArray *maskBounds = [NSMutableArray new];
530
+ NSMutableArray *maskPositions = [NSMutableArray new];
531
+ NSMutableArray *maskRadii = [NSMutableArray new];
532
+ for (NSNumber *time in times) {
533
+ ANNFlightState state = [self stateAtTime:time.doubleValue];
534
+ CGRect whole = CGRectMake(0, 0, state.size.width, state.size.height);
535
+ [bounds addObject:ANNBoundsValue(whole)];
536
+ [positions addObject:[NSValue valueWithCGPoint:state.center]];
537
+ [transforms addObject:[NSValue valueWithCATransform3D:state.transform]];
538
+ [radii addObject:@(state.cornerRadius)];
539
+ [fromBounds addObject:ANNBoundsValue(state.fromFrame)];
540
+ [fromPositions addObject:ANNPositionValue(state.fromFrame)];
541
+ [fromAlphas addObject:@(state.fromAlpha)];
542
+ [toBounds addObject:ANNBoundsValue(state.toFrame)];
543
+ [toPositions addObject:ANNPositionValue(state.toFrame)];
544
+ [toAlphas addObject:@(state.toAlpha)];
545
+ [maskBounds addObject:ANNBoundsValue(state.mask)];
546
+ [maskPositions addObject:ANNPositionValue(state.mask)];
547
+ [maskRadii addObject:@(state.maskRadius)];
548
+ }
549
+ CALayer *copy = _copy.layer;
550
+ ANNAddKeyframes(copy, @"bounds", bounds, times, length);
551
+ ANNAddKeyframes(copy, @"position", positions, times, length);
552
+ ANNAddKeyframes(copy, @"transform", transforms, times, length);
553
+ if (!_text) {
554
+ ANNAddKeyframes(copy, @"cornerRadius", radii, times, length);
555
+ }
556
+ ANNAddKeyframes(_fromPicture.layer, @"bounds", fromBounds, times, length);
557
+ ANNAddKeyframes(_fromPicture.layer, @"position", fromPositions, times, length);
558
+ ANNAddKeyframes(_fromPicture.layer, @"opacity", fromAlphas, times, length);
559
+ ANNAddKeyframes(_toPicture.layer, @"bounds", toBounds, times, length);
560
+ ANNAddKeyframes(_toPicture.layer, @"position", toPositions, times, length);
561
+ ANNAddKeyframes(_toPicture.layer, @"opacity", toAlphas, times, length);
562
+ if (_mask) {
563
+ ANNAddKeyframes(_mask, @"bounds", maskBounds, times, length);
564
+ ANNAddKeyframes(_mask, @"position", maskPositions, times, length);
565
+ ANNAddKeyframes(_mask, @"cornerRadius", maskRadii, times, length);
426
566
  }
427
- _fromPicture.alpha = (CGFloat)(1 - swap);
428
- _toPicture.alpha = (CGFloat)swap;
429
567
  }
430
568
 
431
569
  - (void)restore {
@@ -20,13 +20,14 @@ FOUNDATION_EXPORT CGFloat ANNCameraDistance(void);
20
20
  /// position and bounds, never its frame) moved by its own translation, `alpha` its alpha before any hide, the
21
21
  /// translations, scales and rotations those of its own transform (React Native's `transform`, split into its parts as
22
22
  /// Android's views hold it: the shorter angle, −180…180). A property that doesn't play keeps the view's own value, its
23
- /// skew and perspective stay. Each frame writes only what plays: `alpha` only with an `alpha` track, `layer.transform`
24
- /// only when a property of the transform plays; with `translationX` and `x` on one side the translation wins. The frame
25
- /// is never changed (React keeps the layout). `restore` puts back the alpha and the transform as they were read.
23
+ /// skew and perspective stay. Only what plays animates, in Core Animation (`ANNKeyframes`): `opacity` only with an
24
+ /// `alpha` track, `transform` only when a property of the transform plays; with `translationX` and `x` on one side the
25
+ /// translation wins. The view's own values (its frame, alpha and transform) are never written: the animations show
26
+ /// over them, and `restore` removes the animations.
26
27
  @interface ANNViewTracks : NSObject <ANNFrameClient>
27
28
 
28
29
  /// The playing properties of `side` on `view`. `alphaBefore`: the view's alpha before the core hid it for the
29
- /// transition (the arriving screen), else its alpha now; it is also what `restore` puts back.
30
+ /// transition (the arriving screen), else its alpha now.
30
31
  - (instancetype)initWithView:(UIView *)view
31
32
  side:(ANNViewAnimationOptions *)side
32
33
  alphaBefore:(CGFloat)alphaBefore;
@@ -35,7 +36,7 @@ FOUNDATION_EXPORT CGFloat ANNCameraDistance(void);
35
36
  /// disappearing from it to `to`, or both ends given.
36
37
  - (instancetype)initWithView:(UIView *)view element:(ANNElementTransitionOptions *)element;
37
38
 
38
- /// A linear fade of `view`'s alpha (a dismissed popover with nothing to play); `restore` puts back `alphaBefore`.
39
+ /// A linear fade of `view`'s alpha (a dismissed popover with nothing to play, a transition under Reduce Motion).
39
40
  - (instancetype)initWithView:(UIView *)view
40
41
  fadeFrom:(CGFloat)from
41
42
  to:(CGFloat)to
@@ -93,8 +93,8 @@ static ANNTrack *ANNTrackOf(Double *from, Double *to, Double *duration, Double *
93
93
  CGFloat _ownPerspective;
94
94
  BOOL _animatesTransform;
95
95
  BOOL _tilts;
96
- BOOL _wroteAlpha;
97
- BOOL _wroteTransform;
96
+ BOOL _addedAlpha;
97
+ BOOL _addedTransform;
98
98
  BOOL _restored;
99
99
  NSTimeInterval _endTime;
100
100
  }
@@ -235,17 +235,9 @@ static ANNTrack *ANNTrackOf(Double *from, Double *to, Double *duration, Double *
235
235
  return track ? [track valueAt:time] : [self viewValueOf:property];
236
236
  }
237
237
 
238
- - (void)applyAtTime:(NSTimeInterval)time {
239
- if (_restored) {
240
- return;
241
- }
242
- if (self.animatesAlpha) {
243
- _view.alpha = (CGFloat)[_tracks[ANNViewPropertyAlpha] valueAt:time];
244
- _wroteAlpha = YES;
245
- }
246
- if (!_animatesTransform) {
247
- return;
248
- }
238
+ // The view's transform at `time`: every property of the transform at its value then, the ones that don't play at the
239
+ // view's own.
240
+ - (CATransform3D)transformAtTime:(NSTimeInterval)time {
249
241
  CGFloat scaleX = (CGFloat)[self valueOf:ANNViewPropertyScaleX at:time];
250
242
  CGFloat scaleY = (CGFloat)[self valueOf:ANNViewPropertyScaleY at:time];
251
243
  // The translation wins over the place when a side has both.
@@ -279,10 +271,48 @@ static ANNTrack *ANNTrackOf(Double *from, Double *to, Double *duration, Double *
279
271
  perspective.m34 = _ownPerspective != 0 ? _ownPerspective : -1.0 / ANNCameraDistance();
280
272
  transform = CATransform3DConcat(transform, perspective);
281
273
  }
282
- transform = CATransform3DConcat(transform,
283
- CATransform3DMakeTranslation(translationX, translationY, 0));
284
- _view.layer.transform = transform;
285
- _wroteTransform = YES;
274
+ return CATransform3DConcat(transform,
275
+ CATransform3DMakeTranslation(translationX, translationY, 0));
276
+ }
277
+
278
+ - (void)addAnimationsWithLength:(NSTimeInterval)length {
279
+ if (_restored || !self.hasTracks) {
280
+ return;
281
+ }
282
+ // Every track's start and end is a sample, so a track that starts late or ends early does it on time.
283
+ NSMutableArray<NSNumber *> *marks = [NSMutableArray new];
284
+ for (NSInteger index = 0; index < ANNViewPropertyCount; index++) {
285
+ ANNTrack *track = _tracks[index];
286
+ if (track) {
287
+ [marks addObject:@(track.delay)];
288
+ [marks addObject:@(track.endTime)];
289
+ }
290
+ }
291
+ NSArray<NSNumber *> *times = ANNSampleTimes(_endTime, length, marks);
292
+ CALayer *layer = _view.layer;
293
+ if (self.animatesAlpha) {
294
+ NSMutableArray<NSNumber *> *values = [NSMutableArray arrayWithCapacity:times.count];
295
+ for (NSNumber *time in times) {
296
+ [values addObject:@([_tracks[ANNViewPropertyAlpha] valueAt:time.doubleValue])];
297
+ }
298
+ CAKeyframeAnimation *animation = ANNKeyframes(@"opacity", values, times, length);
299
+ if (animation) {
300
+ [layer addAnimation:animation forKey:ANNAnimationKey(@"opacity")];
301
+ _addedAlpha = YES;
302
+ }
303
+ }
304
+ if (_animatesTransform) {
305
+ NSMutableArray<NSValue *> *values = [NSMutableArray arrayWithCapacity:times.count];
306
+ for (NSNumber *time in times) {
307
+ CATransform3D transform = [self transformAtTime:time.doubleValue];
308
+ [values addObject:[NSValue valueWithCATransform3D:transform]];
309
+ }
310
+ CAKeyframeAnimation *animation = ANNKeyframes(@"transform", values, times, length);
311
+ if (animation) {
312
+ [layer addAnimation:animation forKey:ANNAnimationKey(@"transform")];
313
+ _addedTransform = YES;
314
+ }
315
+ }
286
316
  }
287
317
 
288
318
  - (void)restore {
@@ -290,11 +320,11 @@ static ANNTrack *ANNTrackOf(Double *from, Double *to, Double *duration, Double *
290
320
  return;
291
321
  }
292
322
  _restored = YES;
293
- if (_wroteAlpha) {
294
- _view.alpha = _alphaBefore;
323
+ if (_addedAlpha) {
324
+ [_view.layer removeAnimationForKey:ANNAnimationKey(@"opacity")];
295
325
  }
296
- if (_wroteTransform) {
297
- _view.layer.transform = _transformBefore;
326
+ if (_addedTransform) {
327
+ [_view.layer removeAnimationForKey:ANNAnimationKey(@"transform")];
298
328
  }
299
329
  }
300
330
 
package/ios/CLAUDE.md CHANGED
@@ -44,7 +44,7 @@ Top-level files: `Package.swift` (SwiftPM; `headerFolders` lists every folder wh
44
44
  - Screen: React root, slot, native screen — `Layouts/Screen/`: `ANNComponentViewController`; `React/`: `ANNReactView`, `ANNReactViewCreator`, `Slots/`; `Layouts/External/`
45
45
  - Screen: insets, background, keyboard focus — `Layouts/Screen/`: `ANNScreenEnvironment`, `ANNScreenSafeArea`, `ANNScreenBars`, `ANNScreenPlace` (the safe area of a parked screen), `ANNComponentPresenter` (background, status bar blur), `ANNComponentViewController` (key commands)
46
46
  - Safe area view — `React/`: `ANNSafeAreaViewComponentView`; `Layouts/Screen/`: `ANNScreenReadiness`; `cpp/`
47
- - Stack: queue, commands, back, gestures — `Layouts/Stack/`: `ANNStackQueue`, `ANNStackCommands`, `ANNStackController`, `InteractivePopGestureDelegate`, `ANNStackAnimatingDelegate`
47
+ - Stack: queue, commands, back, gestures — `Layouts/Stack/`: `ANNStackQueue`, `ANNStackCommands`, `ANNStackController`, `InteractivePopGestureDelegate`, `ANNStackAnimatingDelegate`, `ANNSwipePopInteraction`
48
48
  - Top bar: title, buttons, menus, large title — `TopBar/`: `ANNTopBarPresenter`, `ANNBarButtons`, `ANNBarAppearance`, `ANNLargeTitlePresenter`, `ANNBackButtonPresenter`, `ANNTitleView`, `ANNTitleGlassView`
49
49
  - Top bar: components, waits — `TopBar/`: `ANNBarComponentsWait`, `ANNBarBackgroundComponent`, `ANNTitleComponentView`, `ANNBarButtonComponentView`; `React/`: `ANNReactComponentRegistry`
50
50
  - Search — `TopBar/`: `ANNSearchPresenter`, `ANNSearchResultsViewController`
@@ -139,10 +139,17 @@ Top-level files: `Package.swift` (SwiftPM; `headerFolders` lists every folder wh
139
139
  (the root, a modal, an overlay: their record is listed and parked by then), and the modal's end steps inside UIKit's
140
140
  completions and delegate calls (`protectEndOf:showing:step:`, `protectBatch:step:`); the overlay's guard is
141
141
  `protectWaitOf:step:`.
142
- - Each step of a transition's element (a pair's start and take-off, an element's start, every frame and restore
142
+ - Each step of a transition's element (a pair's start and take-off, an element's start, its animations and restore
143
143
  through `ANNIsolatedElement`) runs in `ANNElementStep`: an exception skips that element only. A new step of an
144
- element goes there too, or one bad element stops the transition (a frame's exception ends the app). The elements of
145
- a transition are found in one walk per screen (`ANNTransitionViews`).
144
+ element goes there too, or one bad element stops the transition. The elements of a transition are found in one walk
145
+ per screen (`ANNTransitionViews`).
146
+ - A custom transition runs in Core Animation (`ANNFrameDriver`): each client samples its values once, at the start,
147
+ into keyframe animations under the core's keys (`ANNAnimationKey`), and `restore` removes them. It never writes a
148
+ view's own alpha or transform for a frame; a value the core writes for the transition (the arriving view's hide, a
149
+ hidden flag, a depth) goes back as before. A pop that follows the swipe back also stops the time of the transition's
150
+ container (`startPausedOn:done:`), and the container gets its time back before the clients restore. UIKit runs its
151
+ swipe back without asking the navigation's delegate for a transition (iOS 26 checked), so that pop follows the
152
+ stack's own swipe from the leading edge, and UIKit's swipes stand aside meanwhile (`swipeBackTakes:`).
146
153
  - An `ANNCommandAnswer` that goes away unanswered rejects (`notAnswered:`), but only once nothing holds it: a record,
147
154
  list or queue that keeps an answer still answers it on every exit, or the promise waits as long as the record lives.
148
155
  - On iOS 26 UIKit draws a screen's large title, for a whole pop or swipe back, inside the scroll view the bar follows
@@ -152,9 +159,10 @@ Top-level files: `Package.swift` (SwiftPM; `headerFolders` lists every folder wh
152
159
  - Under a hidden bar UIKit's swipe delegate refuses the swipes in questions of its own: `InteractivePopGestureDelegate`
153
160
  stops passing those on while the bar is hidden (`barHidden`, set again on the recognizer at each change, from
154
161
  `swipeBackFollowsBar`), and the core sorts the pans itself.
155
- - UIKit works out the safe area of the window's root view from where it is drawn: a `transform` that moves it (a
156
- root's own `enter`) leaves it with no top inset, and putting the transform back doesn't work it out again. Lay the
157
- root out after a restore (`+[ANNRootCoordinator stopAnimationOf:]`).
162
+ - UIKit works out the safe area of the window's root view from where it is: a `transform` written on it that moves it
163
+ leaves it with no top inset, and putting the transform back doesn't work it out again. A root's own `enter` moves
164
+ only what Core Animation draws; the root is still laid out after a restore (`+[ANNRootCoordinator stopAnimationOf:]`).
165
+ While it animates, the new root takes no touches: it is drawn elsewhere than where it is.
158
166
  - UIKit's `hidesBarsWhenKeyboardAppears`, once on, keeps hiding the bar after it is set back to `NO`: the stack hides
159
167
  the bars for `topBar.hideOnKeyboard` itself (`keyboardWillShow:` / `keyboardDidHide:` of `ANNStackController`) and
160
168
  leaves the flag off.
package/ios/CORE.md CHANGED
@@ -139,7 +139,7 @@ Each of these owns one rule. Call it; do not copy it.
139
139
  - `ANNReloadCoordinator`: The generation of the JavaScript; the reset after a reload. Called by: Runner, emitter, answer.
140
140
  - Stack answer id: `ANNStackAnswerIdOf(entry, command)` in `ANNStackQueue.h`: the id a stack command answers with. `push` answers the node's own id (a pair's id); every other command, the entry's options screen (a pair's primary). Called by: The queue, `ANNStackCommands`, `ANNPreviewPush`, `ANNSplitViewCommands`.
141
141
  - Waited screens: `waitedComponentViewControllers`: the screens a wait is about (a pair: the ones it puts in the window). `ANNStackScreensOfEntry` is every screen of an entry, for what means all of them. Called by: `ANNRenderWait` owners: root, stack queue, modals, overlays, `ANNSplitViewCommands`.
142
- - Back: The stack: `ANNStackController` (`userGoBackTo:`, `backAction`, the hold) and `InteractivePopGestureDelegate`; UIKit's own pops are followed by `ANNStackQueue uikitShows:`. Called by: UIKit, the user.
142
+ - Back: The stack: `ANNStackController` (`userGoBackTo:`, `backAction`, the hold) and `InteractivePopGestureDelegate`; UIKit's own pops are followed by `ANNStackQueue uikitShows:`. A top whose own pop is `interactive` gets the stack's own swipe from the leading edge instead of UIKit's (UIKit runs its swipe without asking the delegate for a transition): `swipeBackTakes:` picks the swipe, `ownSwipeBack:` pops, and `swipeAnimatorFrom:to:` and `ANNSwipePopInteraction` play the pop under the finger. Called by: UIKit, the user.
143
143
 
144
144
  ## Modules
145
145
 
@@ -71,8 +71,6 @@ static BOOL ANNAppUsesScenes(void) {
71
71
  rootViewFactory.reactHost = host;
72
72
  [ApexNavigation bootstrapWithHost:host];
73
73
  self.window = ApexNavigation.mainWindow;
74
- // The links that come before React content wait for it; the launch link waits for the scene.
75
- [ANNDeepLinkQueue start];
76
74
  if (!ANNAppUsesScenes()) {
77
75
  [self.window makeKeyAndVisible];
78
76
  }
@@ -14,7 +14,8 @@ typedef void (^ANNLaunchLinkCompletion)(NSURL *_Nullable url);
14
14
  */
15
15
  @interface ANNDeepLinkQueue : NSObject
16
16
 
17
- /// Starts the queue as the app launches (`ANNAppDelegate`). An app that never starts it has no launch link.
17
+ /// Starts the queue as the app launches (`+[ApexNavigation bootstrapWithHost:]`, so also under an app delegate of the
18
+ /// app's own). Once: a second call does nothing.
18
19
  + (void)start;
19
20
 
20
21
  /// Hands `url` to `Linking`, or keeps it until React content first appears and no `takeLaunchLink:` waits.
@@ -33,11 +34,11 @@ typedef void (^ANNLaunchLinkCompletion)(NSURL *_Nullable url);
33
34
  ofScene:(UIScene *)scene
34
35
  through:(NS_NOESCAPE dispatch_block_t)forward;
35
36
 
36
- /// Hands the scene's link to `Linking` at once, past the queue: for an app delegate that doesn't take links.
37
- + (void)openURLContext:(UIOpenURLContext *)context ofScene:(UIScene *)scene;
37
+ /// `dispatchURL:` for the scene's link, with its scene and context: for an app delegate that doesn't take links.
38
+ + (void)dispatchURLContext:(UIOpenURLContext *)context ofScene:(UIScene *)scene;
38
39
 
39
- /// `openURLContext:ofScene:` for a web link (`webpageURL`) of the scene; an activity without one does nothing.
40
- + (void)openUserActivity:(NSUserActivity *)activity ofScene:(UIScene *)scene;
40
+ /// `dispatchURLContext:ofScene:` for a web link (`webpageURL`) of the scene; an activity without one does nothing.
41
+ + (void)dispatchUserActivity:(NSUserActivity *)activity ofScene:(UIScene *)scene;
41
42
 
42
43
  /// The app's window scene connected (`+[ApexNavigation attachToWindowScene:]`).
43
44
  + (void)windowSceneDidConnect;
@@ -71,19 +71,19 @@ static ANNLaunchLinkCompletion ANNWaitingLaunchLinkCompletion;
71
71
  selector:@selector(applicationDidBecomeActive:)
72
72
  name:UIApplicationDidBecomeActiveNotification
73
73
  object:nil];
74
+ // Started once the app is active (an app that starts React Native later, on demand), the queue missed the scene's
75
+ // connection and the notification: the links that launched the app went elsewhere, and none waits for them.
76
+ if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive) {
77
+ [self launchLinksKnown];
78
+ }
74
79
  }
75
80
 
76
81
  + (void)dispatchURL:(NSURL *)url {
77
- ANNAssertMainQueue();
78
82
  // The URL of the scene's link keeps its context; one the app delegate changed is a URL of its own.
79
83
  ANNDeepLink *link = [ANNForwardedSceneLink.url isEqual:url]
80
84
  ? ANNForwardedSceneLink
81
85
  : [[ANNDeepLink alloc] initWithURL:url context:nil activity:nil scene:nil];
82
- if (ANNReactContentAppeared && ANNWaitingLaunchLinkCompletion == nil) {
83
- [self openLink:link];
84
- return;
85
- }
86
- [ANNPendingDeepLinks addObject:link];
86
+ [self dispatchLink:link];
87
87
  }
88
88
 
89
89
  + (void)forwardURLContext:(UIOpenURLContext *)context
@@ -104,15 +104,18 @@ static ANNLaunchLinkCompletion ANNWaitingLaunchLinkCompletion;
104
104
  through:forward];
105
105
  }
106
106
 
107
- + (void)openURLContext:(UIOpenURLContext *)context ofScene:(UIScene *)scene {
108
- [self openLink:[[ANNDeepLink alloc] initWithURL:context.URL context:context activity:nil scene:scene]];
107
+ + (void)dispatchURLContext:(UIOpenURLContext *)context ofScene:(UIScene *)scene {
108
+ [self dispatchLink:[[ANNDeepLink alloc] initWithURL:context.URL context:context activity:nil scene:scene]];
109
109
  }
110
110
 
111
- + (void)openUserActivity:(NSUserActivity *)activity ofScene:(UIScene *)scene {
111
+ + (void)dispatchUserActivity:(NSUserActivity *)activity ofScene:(UIScene *)scene {
112
112
  if (activity.webpageURL == nil) {
113
113
  return;
114
114
  }
115
- [self openLink:[[ANNDeepLink alloc] initWithURL:activity.webpageURL context:nil activity:activity scene:scene]];
115
+ [self dispatchLink:[[ANNDeepLink alloc] initWithURL:activity.webpageURL
116
+ context:nil
117
+ activity:activity
118
+ scene:scene]];
116
119
  }
117
120
 
118
121
  + (void)windowSceneDidConnect {
@@ -191,6 +194,15 @@ static ANNLaunchLinkCompletion ANNWaitingLaunchLinkCompletion;
191
194
  completion(url);
192
195
  }
193
196
 
197
+ + (void)dispatchLink:(ANNDeepLink *)link {
198
+ ANNAssertMainQueue();
199
+ if (ANNReactContentAppeared && ANNWaitingLaunchLinkCompletion == nil) {
200
+ [self openLink:link];
201
+ return;
202
+ }
203
+ [ANNPendingDeepLinks addObject:link];
204
+ }
205
+
194
206
  + (void)forwardLink:(ANNDeepLink *)link through:(NS_NOESCAPE dispatch_block_t)forward {
195
207
  ANNAssertMainQueue();
196
208
  ANNDeepLink *outer = ANNForwardedSceneLink;
@@ -77,7 +77,7 @@
77
77
  [appDelegate application:application openURL:context.URL options:options];
78
78
  }];
79
79
  } else {
80
- [ANNDeepLinkQueue openURLContext:context ofScene:scene];
80
+ [ANNDeepLinkQueue dispatchURLContext:context ofScene:scene];
81
81
  }
82
82
  }
83
83
 
@@ -95,7 +95,7 @@
95
95
  }];
96
96
  }];
97
97
  } else if ([activity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
98
- [ANNDeepLinkQueue openUserActivity:activity ofScene:scene];
98
+ [ANNDeepLinkQueue dispatchUserActivity:activity ofScene:scene];
99
99
  }
100
100
  }
101
101
 
@@ -45,8 +45,9 @@ static UIWindowScene *ANNConnectedWindowScene(void) {
45
45
  }
46
46
  }
47
47
 
48
- // Once, on the main queue: the generated native screens before any other part, then the core's parts, the `<Modal>`
49
- // replacement before any React content, the window with the copy of the launch screen, and the watch of the bundles.
48
+ // Once, on the main queue: the generated native screens before any other part, then the core's parts, the queue of the
49
+ // links, the `<Modal>` replacement before any React content, the window with the copy of the launch screen, and the
50
+ // watch of the bundles.
50
51
  + (void)bootstrapWithHost:(RCTHost *)host {
51
52
  ANNAssertMainQueue();
52
53
  if (ANNBootstrapped) {
@@ -55,6 +56,9 @@ static UIWindowScene *ANNConnectedWindowScene(void) {
55
56
  ANNBootstrapped = YES;
56
57
  [self registerGeneratedScreens];
57
58
  ANNHost = host;
59
+ // The links that come before React content wait for it; the launch link waits for the scene. Here, not in
60
+ // `ANNAppDelegate`, so an app delegate of the app's own keeps its launch link.
61
+ [ANNDeepLinkQueue start];
58
62
  // React Native lets a class with the descriptor of `ModalHostView` stand in for its own.
59
63
  [ANNModalHostViewComponentView registerForModalHostView];
60
64
  [self makeMainWindowInScene:nil];