@apexnative/navigation 1.0.0-alpha.1 → 1.0.0-alpha.2

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 (125) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +147 -51
  3. package/android/CLAUDE.md +10 -4
  4. package/android/CORE.md +4 -1
  5. package/android/RECIPES.md +4 -3
  6. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +5 -4
  7. package/android/src/main/java/dev/apexnative/navigation/react/ErrorTexts.kt +75 -60
  8. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +3 -2
  9. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +1 -1
  10. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +5 -4
  11. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +7 -6
  12. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +3 -35
  13. package/android/src/main/java/dev/apexnative/navigation/react/events/EventContract.kt +226 -0
  14. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +101 -87
  15. package/android/src/main/java/dev/apexnative/navigation/react/events/ScreenAppearance.kt +0 -6
  16. package/android/src/main/java/dev/apexnative/navigation/utils/UiUtils.kt +3 -3
  17. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabItems.kt +3 -3
  18. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +12 -8
  19. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +12 -11
  20. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +2 -2
  21. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +25 -0
  22. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +1 -1
  23. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -3
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +4 -7
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuPresenter.kt +16 -7
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuRules.kt +8 -1
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +2 -1
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +2 -1
  29. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +5 -5
  30. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStep.kt +31 -0
  31. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +85 -27
  32. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +5 -4
  33. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionTargets.kt +9 -6
  34. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionViews.kt +100 -0
  35. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +45 -1
  36. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +3 -5
  37. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +3 -5
  38. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +3 -5
  39. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +2 -0
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +4 -6
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +4 -6
  42. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +4 -3
  43. package/cli/app.js +1 -1
  44. package/cli/project.js +6 -2
  45. package/cli/setup/index.js +13 -0
  46. package/cli/setup/steps/appDelegate.js +66 -11
  47. package/cli/setup/steps/sceneDelegate.js +108 -0
  48. package/docs-md/architecture/transitions.md +1 -1
  49. package/docs-md/guides/deep-links.md +15 -33
  50. package/docs-md/start/installation.md +1 -0
  51. package/ios/Animations/ANNTransitionAnimator.mm +13 -11
  52. package/ios/Animations/ANNTransitionWait.mm +6 -7
  53. package/ios/Animations/Elements/ANNElementFinder.h +57 -0
  54. package/ios/Animations/Elements/ANNElementFinder.mm +174 -2
  55. package/ios/Animations/Elements/ANNElementTransition.h +7 -5
  56. package/ios/Animations/Elements/ANNElementTransition.mm +17 -19
  57. package/ios/Animations/Elements/ANNSharedElementFlight.h +13 -8
  58. package/ios/Animations/Elements/ANNSharedElementFlight.mm +59 -24
  59. package/ios/CLAUDE.md +10 -2
  60. package/ios/CORE.md +3 -2
  61. package/ios/Contract/ANNCommandAnswer.h +4 -3
  62. package/ios/Contract/ANNCommandAnswer.mm +12 -2
  63. package/ios/Contract/ANNCommandRunner.h +2 -2
  64. package/ios/Contract/ANNCommandRunner.mm +1 -1
  65. package/ios/Contract/ANNErrorTexts.h +6 -2
  66. package/ios/Contract/ANNErrorTexts.mm +18 -10
  67. package/ios/Core/ANNDeepLinkQueue.h +20 -1
  68. package/ios/Core/ANNDeepLinkQueue.mm +107 -12
  69. package/ios/Core/ANNErrorHandler.h +8 -3
  70. package/ios/Core/ANNErrorHandler.mm +17 -1
  71. package/ios/Core/ANNEventContract.h +246 -0
  72. package/ios/Core/ANNEventContract.mm +225 -0
  73. package/ios/Core/ANNEventEmitter.h +5 -1
  74. package/ios/Core/ANNEventEmitter.mm +117 -80
  75. package/ios/Core/ANNHinge.mm +6 -4
  76. package/ios/Core/ANNSceneDelegate.mm +25 -15
  77. package/ios/Core/ANNSystemUI.h +3 -2
  78. package/ios/Core/ANNSystemUI.mm +7 -0
  79. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +6 -4
  80. package/ios/Layouts/Modal/ANNModalCoordinator.mm +1 -1
  81. package/ios/Layouts/Modal/ANNModalPresentation.h +8 -5
  82. package/ios/Layouts/Modal/ANNModalPresentation.mm +12 -12
  83. package/ios/Layouts/Modal/ANNSheetPresenter.mm +83 -1
  84. package/ios/Layouts/Root/ANNRootCoordinator.mm +12 -3
  85. package/ios/Layouts/Screen/ANNComponentPresenter.h +11 -3
  86. package/ios/Layouts/Screen/ANNComponentPresenter.mm +34 -6
  87. package/ios/Layouts/Screen/ANNComponentViewController.mm +28 -7
  88. package/ios/Layouts/Screen/ANNNativeScreenContext.mm +7 -5
  89. package/ios/Layouts/Screen/ANNScreenInsets.mm +6 -4
  90. package/ios/Layouts/Screen/ANNScreenRegions.mm +9 -6
  91. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -7
  92. package/ios/Options/ANNSheetOptions.h +4 -1
  93. package/ios/Public/ApexNavigation/ANNSceneDelegate.h +3 -0
  94. package/ios/RECIPES.md +2 -1
  95. package/ios/TurboModules/ANNTurboEventEmitter.mm +4 -35
  96. package/ios/TurboModules/ANNTurboModule.mm +5 -2
  97. package/lib/module/ARCHITECTURE.md +7 -4
  98. package/lib/module/api/app/createNavigation.js +2 -0
  99. package/lib/module/api/app/createNavigation.js.map +1 -1
  100. package/lib/module/api/linking/dispatchedLinks.js +23 -0
  101. package/lib/module/api/linking/dispatchedLinks.js.map +1 -0
  102. package/lib/module/interfaces/contract/errors.json +275 -0
  103. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  104. package/lib/typescript/api/linking/dispatchedLinks.d.ts +7 -0
  105. package/lib/typescript/api/linking/dispatchedLinks.d.ts.map +1 -0
  106. package/lib/typescript/interfaces/Events.d.ts +9 -0
  107. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  108. package/lib/typescript/interfaces/options/animations.d.ts +17 -13
  109. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  110. package/lib/typescript/interfaces/options/modals.d.ts +15 -11
  111. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  112. package/lib/typescript/interfaces/options/sideMenu.d.ts +7 -6
  113. package/lib/typescript/interfaces/options/sideMenu.d.ts.map +1 -1
  114. package/lib/typescript/interfaces/options/system.d.ts +4 -3
  115. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  116. package/package.json +1 -1
  117. package/src/ARCHITECTURE.md +7 -4
  118. package/src/api/app/createNavigation.ts +2 -0
  119. package/src/api/linking/dispatchedLinks.ts +18 -0
  120. package/src/interfaces/Events.ts +10 -0
  121. package/src/interfaces/contract/errors.json +275 -0
  122. package/src/interfaces/options/animations.ts +17 -13
  123. package/src/interfaces/options/modals.ts +15 -11
  124. package/src/interfaces/options/sideMenu.ts +7 -6
  125. package/src/interfaces/options/system.ts +4 -3
@@ -118,21 +118,21 @@ static NSDictionary<NSString *, NSNumber *> *ANNTransitionStyles(void) {
118
118
  return nil;
119
119
  }
120
120
 
121
- // UIKit takes `partialCurl` only for a full-screen modal over a controller that fills the screen, and a controller
122
- // shown with `partialCurl` presents nothing more: the command rejects before it asks UIKit.
123
- - (NSString *)problemPresentingFrom:(UIViewController *)presenting
124
- windowRoot:(UIViewController *)windowRoot {
121
+ // UIKit curls only a full-screen modal over a controller that fills the screen: elsewhere `partialCurl` is
122
+ // `coverVertical`, so the modal still shows. A controller shown with `partialCurl` presents nothing more: the command
123
+ // rejects before it asks UIKit.
124
+ - (NSString *)fitToPresenting:(UIViewController *)presenting
125
+ windowRoot:(UIViewController *)windowRoot {
125
126
  if (presenting.presentingViewController &&
126
127
  presenting.modalTransitionStyle == UIModalTransitionStylePartialCurl) {
127
- return @"partialCurl needs a full screen modal";
128
+ return @"a modal shown with partialCurl can't show another modal";
128
129
  }
129
- if (_transitionStyle != UIModalTransitionStylePartialCurl) {
130
- return nil;
131
- }
132
- BOOL presentingFills = presenting == windowRoot ||
133
- presenting.modalPresentationStyle == UIModalPresentationFullScreen;
134
- if (_style != UIModalPresentationFullScreen || !presentingFills) {
135
- return @"partialCurl needs a full screen modal";
130
+ if (_transitionStyle == UIModalTransitionStylePartialCurl) {
131
+ BOOL presentingFills = presenting == windowRoot ||
132
+ presenting.modalPresentationStyle == UIModalPresentationFullScreen;
133
+ if (_style != UIModalPresentationFullScreen || !presentingFills) {
134
+ _transitionStyle = UIModalTransitionStyleCoverVertical;
135
+ }
136
136
  }
137
137
  return nil;
138
138
  }
@@ -1,6 +1,7 @@
1
1
  #import "ANNSheetPresenter.h"
2
2
  #import "ANNModalSizePresenter.h"
3
3
  #import "ANNReactView.h"
4
+ #import "ANNUtils.h"
4
5
  #import "UIViewController+LayoutProtocol.h"
5
6
  #import <React/RCTLog.h>
6
7
  #import <objc/runtime.h>
@@ -53,7 +54,7 @@ static NSString *const ANNContentDetent = @"content";
53
54
  + (void)applyOptions:(ANNSheetOptions *)options toSheet:(UISheetPresentationController *)sheet {
54
55
  if (options.detents) {
55
56
  NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray new];
56
- for (id value in options.detents) {
57
+ for (id value in [self sortedDetents:options.detents ofSheet:sheet]) {
57
58
  UISheetPresentationControllerDetent *detent = [self detentFor:value ofSheet:sheet];
58
59
  if (detent) {
59
60
  [detents addObject:detent];
@@ -81,6 +82,85 @@ static NSString *const ANNContentDetent = @"content";
81
82
  }
82
83
  }
83
84
 
85
+ // UIKit wants the detents once each, from the lowest to the highest: it takes the last for the largest and closes the
86
+ // sheet on a drag down from the first. The options may give them in any order and more than once; the first of a
87
+ // repeat stays, and detents of the same height keep their order.
88
+ + (NSArray *)sortedDetents:(NSArray *)values ofSheet:(UISheetPresentationController *)sheet {
89
+ NSMutableArray *detents = [NSMutableArray new];
90
+ NSMutableSet<NSString *> *identifiers = [NSMutableSet new];
91
+ for (id value in values) {
92
+ NSString *identifier = [self identifierFor:value];
93
+ if (identifier && ![identifiers containsObject:identifier]) {
94
+ [identifiers addObject:identifier];
95
+ [detents addObject:value];
96
+ }
97
+ }
98
+ CGFloat maximum = [self maximumDetentHeightOf:sheet];
99
+ return [detents sortedArrayWithOptions:NSSortStable
100
+ usingComparator:^NSComparisonResult(id a, id b) {
101
+ return [self compareHeightOf:a
102
+ with:b
103
+ maximum:maximum
104
+ ofSheet:sheet];
105
+ }];
106
+ }
107
+
108
+ // The content of a sheet sized to it changed its height, and with it maybe its place among the other detents.
109
+ + (void)sortDetentsOfSheet:(UISheetPresentationController *)sheet API_AVAILABLE(ios(16.0)) {
110
+ CGFloat maximum = [self maximumDetentHeightOf:sheet];
111
+ NSArray<UISheetPresentationControllerDetent *> *detents = sheet.detents;
112
+ NSArray<UISheetPresentationControllerDetent *> *sorted = [detents
113
+ sortedArrayWithOptions:NSSortStable
114
+ usingComparator:^NSComparisonResult(UISheetPresentationControllerDetent *a,
115
+ UISheetPresentationControllerDetent *b) {
116
+ return [self compareHeightOf:[self detentForIdentifier:a.identifier]
117
+ with:[self detentForIdentifier:b.identifier]
118
+ maximum:maximum
119
+ ofSheet:sheet];
120
+ }];
121
+ if (![sorted isEqualToArray:detents]) {
122
+ sheet.detents = sorted;
123
+ }
124
+ }
125
+
126
+ + (NSComparisonResult)compareHeightOf:(id)a
127
+ with:(id)b
128
+ maximum:(CGFloat)maximum
129
+ ofSheet:(UISheetPresentationController *)sheet {
130
+ CGFloat heightA = [self heightOf:a maximum:maximum ofSheet:sheet];
131
+ CGFloat heightB = [self heightOf:b maximum:maximum ofSheet:sheet];
132
+ return heightA < heightB ? NSOrderedAscending
133
+ : heightA > heightB ? NSOrderedDescending : NSOrderedSame;
134
+ }
135
+
136
+ // The height a detent of the options stands at, to sort them, as its resolver gives it: 'medium' half of the largest
137
+ // height (UIKit's is about half), 'large' all of it, a height capped at it, 'content' the content's height now, all of
138
+ // it while the content isn't measured.
139
+ + (CGFloat)heightOf:(id)value
140
+ maximum:(CGFloat)maximum
141
+ ofSheet:(UISheetPresentationController *)sheet {
142
+ if ([value isEqual:@"medium"]) {
143
+ return maximum / 2;
144
+ }
145
+ if ([value isKindOfClass:NSNumber.class]) {
146
+ return MIN([value doubleValue], maximum);
147
+ }
148
+ if ([value isEqual:ANNContentDetent]) {
149
+ CGFloat height = [self contentHeightOf:sheet.presentedViewController];
150
+ return height > 0 ? MIN(height, maximum) : maximum;
151
+ }
152
+ return maximum;
153
+ }
154
+
155
+ // About the largest height of a sheet: its container, or the window before it is shown, below the top safe area.
156
+ + (CGFloat)maximumDetentHeightOf:(UISheetPresentationController *)sheet {
157
+ UIView *view = sheet.containerView ?: ANNUtils.mainWindow;
158
+ if (!view) {
159
+ return ANNUtils.mainScreen.bounds.size.height;
160
+ }
161
+ return view.bounds.size.height - view.safeAreaInsets.top;
162
+ }
163
+
84
164
  + (UISheetPresentationControllerDetent *)detentFor:(id)value
85
165
  ofSheet:(UISheetPresentationController *)sheet {
86
166
  if ([value isEqual:@"medium"]) {
@@ -180,6 +260,7 @@ static NSString *const ANNContentDetent = @"content";
180
260
  }
181
261
  height = newHeight;
182
262
  [strongSheet animateChanges:^{
263
+ [self sortDetentsOfSheet:strongSheet];
183
264
  [strongSheet invalidateDetents];
184
265
  }];
185
266
  }
@@ -201,6 +282,7 @@ static NSString *const ANNContentDetent = @"content";
201
282
  // The new screen's content mounts may come later: its mounts resolve the detents again too.
202
283
  [self followContentOfSheet:sheet when:YES];
203
284
  [sheet animateChanges:^{
285
+ [self sortDetentsOfSheet:sheet];
204
286
  [sheet invalidateDetents];
205
287
  }];
206
288
  }
@@ -69,7 +69,10 @@ typedef NS_ENUM(NSInteger, ANNRootShowState) {
69
69
  // The nodes whose appearance the transition reports itself (`driveAppearanceOf:`), while `driving`.
70
70
  @property(nonatomic, copy) NSArray<UIViewController *> *driven;
71
71
  @property(nonatomic) BOOL driving;
72
+ // The snapshot of the window as it was: it dissolves over the new root (`dissolves`), plays its `exit` (`custom`), or
73
+ // only covers the old root's modals while they go, and leaves as the new root is the window's.
72
74
  @property(nonatomic, strong, nullable) UIView *snapshot;
75
+ @property(nonatomic) BOOL dissolves;
73
76
  // The root is the window's root: the old root's modals went before it (`startTransitionOf:done:`).
74
77
  @property(nonatomic) BOOL installed;
75
78
  // The root's view waits in the window under the current root (`park:`).
@@ -415,9 +418,14 @@ static __weak ANNRootShow *ANNAnimatingShow;
415
418
  BOOL seen = !window.hidden && scene != nil &&
416
419
  scene.activationState != UISceneActivationStateBackground;
417
420
  BOOL dissolves = !overSplash && !enterDisabled && !custom && seen;
421
+ show.dissolves = dissolves;
418
422
  show.custom = custom && seen;
419
- UIView *snapshot =
420
- dissolves || show.custom ? [window snapshotViewAfterScreenUpdates:NO] : nil;
423
+ // UIKit draws the dismissal of the old root's modals, without animation too (a sheet's dim goes a frame before
424
+ // its card): with nothing to dissolve, the snapshot covers them until the new root is in place.
425
+ BOOL covers = seen && old.presentedViewController != nil;
426
+ UIView *snapshot = dissolves || show.custom || covers
427
+ ? [window snapshotViewAfterScreenUpdates:NO]
428
+ : nil;
421
429
 
422
430
  show.state = ANNRootShowStateTransition;
423
431
  // It shows from here: a stack in it counts as on screen.
@@ -480,7 +488,8 @@ static __weak ANNRootShow *ANNAnimatingShow;
480
488
  [self endTransitionOf:show];
481
489
  return;
482
490
  }
483
- if (!snapshot) {
491
+ // A snapshot that only covered the old root's modals leaves in this turn, with the new root in place under it.
492
+ if (!snapshot || !show.dissolves) {
484
493
  [self endTransitionOf:show];
485
494
  return;
486
495
  }
@@ -19,11 +19,19 @@ NS_ASSUME_NONNULL_BEGIN
19
19
  /// it. Nothing before the screen's view is loaded.
20
20
  - (void)applyBackground:(ANNNavigationOptions *)resolvedOptions;
21
21
 
22
- /// `statusBar.blur` (iOS): UIKit's regular blur over the screen's own view in the frame of the scene's status bar at
23
- /// this moment (x 0, the view's width; it doesn't follow a rotation); `false` or none removes it. At each appearance
24
- /// and at a merge that names it; nothing before the screen's view is loaded.
22
+ /// `statusBar.blur` (iOS): UIKit's regular blur over the screen's own view in the frame of the scene's status bar (x 0,
23
+ /// the view's width), laid out at once; `false` or none removes it. At each appearance and at a merge that names it;
24
+ /// nothing before the screen's view is loaded.
25
25
  - (void)applyStatusBarBlur:(ANNNavigationOptions *)resolvedOptions;
26
26
 
27
+ /// The blur takes the status bar's frame as it is now; nothing without a blur. The single place of its frame: the blur
28
+ /// as it is applied, the screen's view laid out (a rotation, a new window size, a new safe area), a size transition
29
+ /// along its animation and at its end, the turn after new traits, and `statusBarDidChange`.
30
+ - (void)layOutStatusBarBlur;
31
+
32
+ /// The status bar was asked again (hidden or shown, a new style): every blur takes its frame.
33
+ + (void)statusBarDidChange;
34
+
27
35
  /// The blur stays the last subview of the screen's view: the content added after it goes under it.
28
36
  - (void)keepStatusBarBlurOnTop;
29
37
 
@@ -5,6 +5,10 @@
5
5
  #import "ANNTopBarPresenter.h"
6
6
  #import "ANNUtils.h"
7
7
 
8
+ // The presenters whose screen shows `statusBar.blur`: the status bar hidden or shown lays out no view, so they hear of
9
+ // it from the system UI (`statusBarDidChange`).
10
+ static NSHashTable<ANNComponentPresenter *> *ANNBlurringPresenters;
11
+
8
12
  @implementation ANNComponentPresenter {
9
13
  // `statusBar.blur` while it is on.
10
14
  UIVisualEffectView *_statusBarBlur;
@@ -95,9 +99,7 @@
95
99
 
96
100
  #pragma mark - The status bar's blur (`statusBar.blur`)
97
101
 
98
- // Over the screen's own view, never over a React view; no touches, nothing for VoiceOver. The frame is the scene's
99
- // status bar's as it is now, in the view's own coordinates without a conversion: a pushed screen's view may still be
100
- // out of place while it appears.
102
+ // Over the screen's own view, never over a React view; no touches, nothing for VoiceOver.
101
103
  - (void)applyStatusBarBlur:(ANNNavigationOptions *)resolvedOptions {
102
104
  ANNComponentViewController *screen = self.screen;
103
105
  if (!screen.isViewLoaded) {
@@ -106,6 +108,7 @@
106
108
  if (![resolvedOptions.statusBar.blur withDefault:NO]) {
107
109
  [_statusBarBlur removeFromSuperview];
108
110
  _statusBarBlur = nil;
111
+ [ANNBlurringPresenters removeObject:self];
109
112
  return;
110
113
  }
111
114
  UIView *view = screen.view;
@@ -115,15 +118,40 @@
115
118
  _statusBarBlur.userInteractionEnabled = NO;
116
119
  _statusBarBlur.isAccessibilityElement = NO;
117
120
  _statusBarBlur.accessibilityElementsHidden = YES;
121
+ if (!ANNBlurringPresenters) {
122
+ ANNBlurringPresenters = [NSHashTable weakObjectsHashTable];
123
+ }
124
+ [ANNBlurringPresenters addObject:self];
118
125
  }
119
- CGRect statusBar = ANNUtils.statusBarFrame;
120
- _statusBarBlur.frame = CGRectMake(0, CGRectGetMinY(statusBar), CGRectGetWidth(view.bounds),
121
- CGRectGetHeight(statusBar));
122
126
  if (_statusBarBlur.superview != view) {
123
127
  [view addSubview:_statusBarBlur];
124
128
  } else {
125
129
  [view bringSubviewToFront:_statusBarBlur];
126
130
  }
131
+ [self layOutStatusBarBlur];
132
+ }
133
+
134
+ // The status bar's frame in the scene of the screen's window (the main window's before the screen is in one), in the
135
+ // view's own coordinates without a conversion: a pushed screen's view may still be out of place while it appears. A
136
+ // hidden status bar has no frame, and the blur no height.
137
+ - (void)layOutStatusBarBlur {
138
+ UIView *view = _statusBarBlur.superview;
139
+ if (!view) {
140
+ return;
141
+ }
142
+ UIWindowScene *scene = view.window.windowScene;
143
+ CGRect statusBar = scene ? scene.statusBarManager.statusBarFrame : ANNUtils.statusBarFrame;
144
+ CGRect frame = CGRectMake(0, CGRectGetMinY(statusBar), CGRectGetWidth(view.bounds),
145
+ CGRectGetHeight(statusBar));
146
+ if (!CGRectEqualToRect(_statusBarBlur.frame, frame)) {
147
+ _statusBarBlur.frame = frame;
148
+ }
149
+ }
150
+
151
+ + (void)statusBarDidChange {
152
+ for (ANNComponentPresenter *presenter in ANNBlurringPresenters.allObjects) {
153
+ [presenter layOutStatusBarBlur];
154
+ }
127
155
  }
128
156
 
129
157
  - (void)keepStatusBarBlurOnTop {
@@ -3,6 +3,7 @@
3
3
  #import "ANNArrangementState.h"
4
4
  #import "ANNBottomTabsController.h"
5
5
  #import "ANNComponentPresenter.h"
6
+ #import "ANNEventContract.h"
6
7
  #import "ANNHinge.h"
7
8
  #import "ANNKeyCommands.h"
8
9
  #import "ANNNodeRegistry.h"
@@ -66,6 +67,12 @@
66
67
 
67
68
  #pragma mark - The view
68
69
 
70
+ - (nullable ANNComponentPresenter *)componentPresenter {
71
+ ANNNodePresenter *presenter = self.presenter;
72
+ return [presenter isKindOfClass:ANNComponentPresenter.class] ? (ANNComponentPresenter *)presenter
73
+ : nil;
74
+ }
75
+
69
76
  // The size of the screen of the navigation window's scene, not the deprecated main screen: a surface made before the
70
77
  // screen is in a window starts with the width it will have.
71
78
  - (void)loadView {
@@ -94,6 +101,22 @@
94
101
  }
95
102
  // The pair the screen is in may lie otherwise now.
96
103
  [self.arrangement screenDidChangeLayout:self];
104
+ [self.componentPresenter layOutStatusBarBlur];
105
+ }
106
+
107
+ // UIKit names the size transition as the moment the status bar's frame changes (a rotation, a new window size): the
108
+ // blur follows it along the animation and takes the final frame at its end.
109
+ - (void)viewWillTransitionToSize:(CGSize)size
110
+ withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
111
+ [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
112
+ __weak ANNComponentViewController *weakSelf = self;
113
+ [coordinator
114
+ animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
115
+ [weakSelf.componentPresenter layOutStatusBarBlur];
116
+ }
117
+ completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
118
+ [weakSelf.componentPresenter layOutStatusBarBlur];
119
+ }];
97
120
  }
98
121
 
99
122
  - (void)viewSafeAreaInsetsDidChange {
@@ -110,6 +133,7 @@
110
133
  __weak ANNComponentViewController *weakSelf = self;
111
134
  dispatch_async(dispatch_get_main_queue(), ^{
112
135
  [weakSelf updateRootLayout];
136
+ [weakSelf.componentPresenter layOutStatusBarBlur];
113
137
  });
114
138
  }
115
139
 
@@ -200,9 +224,7 @@
200
224
  #pragma mark - The root frame and the safe area
201
225
 
202
226
  - (void)applyBackground {
203
- if ([self.presenter isKindOfClass:ANNComponentPresenter.class]) {
204
- [(ANNComponentPresenter *)self.presenter applyBackground:[self resolveOptionsWithDefault]];
205
- }
227
+ [self.componentPresenter applyBackground:[self resolveOptionsWithDefault]];
206
228
  }
207
229
 
208
230
  // The navigation controller that shows the screen's stack: the stack, or UIKit's More navigation while a tab of More
@@ -434,7 +456,8 @@
434
456
  if (!regions) {
435
457
  return;
436
458
  }
437
- BOOL empty = [regions[@"division"] count] == 0 && [regions[@"occlusion"] count] == 0;
459
+ const auto &keys = ANNEventContract.ScreenRegionsChanged;
460
+ BOOL empty = [regions[keys.division] count] == 0 && [regions[keys.occlusion] count] == 0;
438
461
  if (empty && ![ANNScreenRegions regionsForScreenId:componentId]) {
439
462
  return;
440
463
  }
@@ -606,9 +629,7 @@
606
629
  content.autoresizingMask = UIViewAutoresizingNone;
607
630
  content.frame = [self rootFrameForOptions:[self resolveOptionsWithDefault]];
608
631
  [view addSubview:content];
609
- if ([self.presenter isKindOfClass:ANNComponentPresenter.class]) {
610
- [(ANNComponentPresenter *)self.presenter keepStatusBarBlurOnTop];
611
- }
632
+ [self.componentPresenter keepStatusBarBlurOnTop];
612
633
  [self applyBackground];
613
634
  [self updateRootLayout];
614
635
  }
@@ -1,5 +1,6 @@
1
1
  #import "ANNNativeScreenContext+Internal.h"
2
2
  #import "ANNErrorTexts.h"
3
+ #import "ANNEventContract.h"
3
4
  #import "ANNEventEmitter.h"
4
5
  #import "ANNNativeScreenCalls.h"
5
6
  #import <React/RCTUtils.h>
@@ -71,12 +72,13 @@
71
72
  componentId:self->_componentId]];
72
73
  return;
73
74
  }
75
+ const auto &keys = ANNEventContract.NativeScreenCall;
74
76
  [eventEmitter sendNativeScreenCall:@{
75
- @"componentId" : self->_componentId,
76
- @"callId" : callId,
77
- @"kind" : kind,
78
- @"name" : name,
79
- @"args" : arguments ?: @[],
77
+ keys.componentId : self->_componentId,
78
+ keys.callId : callId,
79
+ keys.kind : kind,
80
+ keys.name : name,
81
+ keys.args : arguments ?: @[],
80
82
  }];
81
83
  });
82
84
  }
@@ -1,4 +1,5 @@
1
1
  #import "ANNScreenInsets.h"
2
+ #import "ANNEventContract.h"
2
3
 
3
4
  NSNotificationName const ANNScreenSafeAreaPaddingDidChangeNotification = @"ANNScreenSafeAreaPaddingDidChange";
4
5
 
@@ -26,11 +27,12 @@ static NSMutableDictionary<NSString *, NSValue *> *ANNScreenPaddingById;
26
27
  object:screenId];
27
28
  }
28
29
  // JavaScript gets what still covers the content: the edges the safe area view keeps it off are 0.
30
+ const auto &keys = ANNEventContract.ScreenInsetsChanged;
29
31
  NSDictionary<NSString *, NSNumber *> *value = @{
30
- @"top" : @(MAX(insets.top - padding.top, 0)),
31
- @"right" : @(MAX(insets.right - padding.right, 0)),
32
- @"bottom" : @(MAX(insets.bottom - padding.bottom, 0)),
33
- @"left" : @(MAX(insets.left - padding.left, 0)),
32
+ keys.top : @(MAX(insets.top - padding.top, 0)),
33
+ keys.right : @(MAX(insets.right - padding.right, 0)),
34
+ keys.bottom : @(MAX(insets.bottom - padding.bottom, 0)),
35
+ keys.left : @(MAX(insets.left - padding.left, 0)),
34
36
  };
35
37
  @synchronized(ANNScreenInsetsById) {
36
38
  if ([ANNScreenInsetsById[screenId] isEqualToDictionary:value]) {
@@ -1,4 +1,5 @@
1
1
  #import "ANNScreenRegions.h"
2
+ #import "ANNEventContract.h"
2
3
 
3
4
  static NSMutableDictionary<NSString *, NSDictionary<NSString *, NSArray *> *> *ANNScreenRegionsById;
4
5
 
@@ -36,11 +37,12 @@ ANNRegionRects(NSArray<UIViewReservedRegion *> *regions, UIView *view, UIView *c
36
37
  if (CGRectIsNull(frame)) {
37
38
  continue;
38
39
  }
40
+ const auto &keys = ANNEventContract.ScreenRegionRect;
39
41
  [rects addObject:@{
40
- @"x" : @(frame.origin.x),
41
- @"y" : @(frame.origin.y),
42
- @"width" : @(frame.size.width),
43
- @"height" : @(frame.size.height),
42
+ keys.x : @(frame.origin.x),
43
+ keys.y : @(frame.origin.y),
44
+ keys.width : @(frame.size.width),
45
+ keys.height : @(frame.size.height),
44
46
  }];
45
47
  }
46
48
  return rects;
@@ -63,9 +65,10 @@ ANNRegionRects(NSArray<UIViewReservedRegion *> *regions, UIView *view, UIView *c
63
65
  [view reservedRegionsOfKind:[UIViewReservedRegionKind divisionRegionKind]];
64
66
  NSArray<UIViewReservedRegion *> *occlusion =
65
67
  [view reservedRegionsOfKind:[UIViewReservedRegionKind occlusionRegionKind]];
68
+ const auto &keys = ANNEventContract.ScreenRegionsChanged;
66
69
  return @{
67
- @"division" : ANNRegionRects(division, view, coordinateView),
68
- @"occlusion" : ANNRegionRects(occlusion, view, coordinateView),
70
+ keys.division : ANNRegionRects(division, view, coordinateView),
71
+ keys.occlusion : ANNRegionRects(occlusion, view, coordinateView),
69
72
  };
70
73
  }
71
74
  #endif
@@ -1,5 +1,6 @@
1
1
  #import "ANNSplitViewController.h"
2
2
  #import "ANNErrorHandler.h"
3
+ #import "ANNErrorTexts.h"
3
4
  #import "ANNShownScreens.h"
4
5
  #import "UISplitViewController+ANNOptions.h"
5
6
  #import "UIViewController+LayoutProtocol.h"
@@ -213,19 +214,15 @@ static UISplitViewControllerColumn ANNSplitViewColumn(NSString *name) {
213
214
  completion:(ANNTransitionCompletionBlock)completion
214
215
  rejection:(ANNTransitionRejectionBlock)rejection {
215
216
  UIViewController *previous = _columns[column];
217
+ NSString *fromId = fromViewController.layoutInfo.componentId;
216
218
  if (!previous) {
217
- rejection([NSString stringWithFormat:@"the split view of the screen '%@' has no '%@' column",
218
- fromViewController.layoutInfo.componentId, column],
219
- nil);
219
+ rejection([ANNErrorTexts noSuchColumn:fromId column:column], nil);
220
220
  return;
221
221
  }
222
222
  if ([previous isKindOfClass:UINavigationController.class]) {
223
223
  // UIKit does not take a navigation controller among the screens of another.
224
224
  if ([screen isKindOfClass:UINavigationController.class]) {
225
- rejection([NSString stringWithFormat:@"the '%@' column of the split view of the screen '%@' is a "
226
- @"stack: it takes a screen, not a stack",
227
- column, fromViewController.layoutInfo.componentId],
228
- nil);
225
+ rejection([ANNErrorTexts columnIsStack:fromId column:column], nil);
229
226
  return;
230
227
  }
231
228
  // In place, as a column of UIKit apps changes; UIKit animates a column it brings on top.
@@ -7,7 +7,10 @@
7
7
  */
8
8
  @interface ANNSheetOptions : ANNOptions
9
9
 
10
- /** The detents as the options give them: NSString ('medium', 'large', 'content') or NSNumber. */
10
+ /**
11
+ * The detents as the options give them, in any order and with repeats: NSString ('medium', 'large', 'content') or
12
+ * NSNumber. ANNSheetPresenter sorts them and drops the repeats.
13
+ */
11
14
  @property(nonatomic, strong) NSArray *detents;
12
15
  @property(nonatomic, strong) Param *selectedDetent;
13
16
  /** The largest detent the screen below stays undimmed and touchable at. */
@@ -9,6 +9,9 @@ NS_ASSUME_NONNULL_BEGIN
9
9
  * When the scene connects, the navigation window moves into it. URLs, user activities and foreground/background
10
10
  * changes that UIKit now delivers to the scene are forwarded to the app delegate's usual methods, so
11
11
  * the deep links (`dispatchDeepLinkURL:`) and app delegate overrides keep working.
12
+ *
13
+ * The app's own scene delegate can extend it (`class SceneDelegate: ANNSceneDelegate {}`, which `setup` makes of React
14
+ * Native 0.88's template); a method it overrides calls `super`.
12
15
  */
13
16
  @interface ANNSceneDelegate : UIResponder <UIWindowSceneDelegate>
14
17
 
package/ios/RECIPES.md CHANGED
@@ -34,7 +34,8 @@ you do exactly that; the map is `ios/CLAUDE.md`.
34
34
  4. **The layout check:** a new node type goes into `ANNLayoutType` and `ANNLayoutReader` (its place, its children), and
35
35
  gets a maker in `ANNLayoutFactory`.
36
36
  5. **Error texts** come from `ANNErrorTexts`, one sentence per case, the same as Android; a text deep in a maker is
37
- thrown with `+[ANNErrorTexts rejection:]`.
37
+ thrown with `+[ANNErrorTexts rejection:]`. A new text goes into `src/interfaces/contract/errors.json`, and
38
+ `node scripts/contract.mjs` makes `ANNErrorTexts` (and Android's `ErrorTexts`) from it; never edit them by hand.
38
39
  6. **The answer:** end through `ANNAppearanceDispatcher` (`endTransitionHiding:…answer:value:`) or call
39
40
  `-[ANNCommandAnswer resolve:]`; the state and `ANN.CommandCompleted` follow by themselves for the names in
40
41
  `ANNCompletedCommandNames`. Add the name there when the command shows or dismisses.
@@ -1,5 +1,6 @@
1
1
  #import "ANNTurboEventEmitter.h"
2
2
  #import "ANNAssert.h"
3
+ #import "ANNEventContract.h"
3
4
  #import "ANNEventEmitter.h"
4
5
  #import "ANNReloadCoordinator.h"
5
6
  #import <React/RCTUtils.h>
@@ -40,41 +41,9 @@ RCT_EXPORT_MODULE(ANNTurboEventEmitter)
40
41
  return self;
41
42
  }
42
43
 
43
- // Every event of the navigation and of the modules.
44
+ // Every event of the navigation and of the modules, as the types of `src/interfaces/` name them.
44
45
  - (NSArray<NSString *> *)supportedEvents {
45
- return @[
46
- @"ANN.AppLaunched",
47
- @"ANN.ArrangementChanged",
48
- @"ANN.BottomAccessoryPlacementChanged",
49
- @"ANN.BottomTabLongPressed",
50
- @"ANN.BottomTabPressed",
51
- @"ANN.BottomTabSelected",
52
- @"ANN.CommandCompleted",
53
- @"ANN.ComponentDidAppear",
54
- @"ANN.ComponentDidDisappear",
55
- @"ANN.ComponentWillAppear",
56
- @"ANN.HingeChanged",
57
- @"ANN.KeyCommandPressed",
58
- @"ANN.ModalAttemptedToDismiss",
59
- @"ANN.ModalDismissed",
60
- @"ANN.NativeScreenCall",
61
- @"ANN.NativeScreenReleased",
62
- @"ANN.NavigationButtonPressed",
63
- @"ANN.NavigationStateChanged",
64
- @"ANN.PreviewCompleted",
65
- @"ANN.ScreenInsetsChanged",
66
- @"ANN.ScreenPopped",
67
- @"ANN.ScreenRegionsChanged",
68
- @"ANN.SearchBarCancelPressed",
69
- @"ANN.SearchBarSubmitted",
70
- @"ANN.SearchBarUpdated",
71
- @"ANN.SearchSuggestionPicked",
72
- @"ANN.SheetDetentChanged",
73
- @"ANN.SlotDestroyed",
74
- @"ANN.SplitViewColumnsStacked",
75
- @"ANN.SplitViewDisplayModeChanged",
76
- @"ANN.TitleRenamed",
77
- ];
46
+ return ANNEventContractNames();
78
47
  }
79
48
 
80
49
  #pragma mark - Listeners
@@ -99,7 +68,7 @@ RCT_EXPORT_MODULE(ANNTurboEventEmitter)
99
68
 
100
69
  - (void)addListener:(NSString *)eventName {
101
70
  [super addListener:eventName];
102
- if ([eventName isEqualToString:@"ANN.AppLaunched"] && !_listensToAppLaunched) {
71
+ if ([eventName isEqualToString:ANNEventContract.AppLaunched.event] && !_listensToAppLaunched) {
103
72
  _listensToAppLaunched = YES;
104
73
  [ANNEventEmitter eventEmitterListensToAppLaunched:self];
105
74
  }
@@ -5,7 +5,9 @@
5
5
  #import "ANNComponentViewController.h"
6
6
  #import "ANNDeepLinkQueue.h"
7
7
  #import "ANNDefaultOptions.h"
8
+ #import "ANNErrorHandler.h"
8
9
  #import "ANNErrorTexts.h"
10
+ #import "ANNEventContract.h"
9
11
  #import "ANNExternalViewController.h"
10
12
  #import "ANNHinge.h"
11
13
  #import "ANNLayoutReader.h"
@@ -248,7 +250,7 @@ RCT_EXPORT_MODULE(ANNTurboModule)
248
250
  layout:(NSDictionary *)layout
249
251
  resolve:(RCTPromiseResolveBlock)resolve
250
252
  reject:(RCTPromiseRejectBlock)reject {
251
- resolve(nil);
253
+ [ANNErrorHandler resolve:resolve of:@"preparePush" value:nil];
252
254
  }
253
255
 
254
256
  - (void)cancelPreparedPush:(NSString *)componentId {
@@ -511,7 +513,8 @@ RCT_EXPORT_MODULE(ANNTurboModule)
511
513
 
512
514
  - (NSDictionary *)getScreenRegions:(NSString *)componentId {
513
515
  NSDictionary *regions = [ANNScreenRegions regionsForScreenId:componentId];
514
- return regions ?: @{@"division" : @[], @"occlusion" : @[]};
516
+ const auto &keys = ANNEventContract.ScreenRegionsChanged;
517
+ return regions ?: @{keys.division : @[], keys.occlusion : @[]};
515
518
  }
516
519
 
517
520
  // `setHingeUpdates`: JavaScript counts its `useHinge` hooks; the events go with the emitter of the JavaScript that runs
@@ -63,7 +63,8 @@ form of `src/api/events/navigationEvents.ts`. Everything in `src/engine` is inte
63
63
  `LinkParams`, `PendingLink`, …). `linkPaths.ts`: a URL into its path and query (`parseLink`), the patterns ordered
64
64
  (`compileRoutes`) and matched (`matchLink`). `linking.ts`: the links of the app — the launch link, the `url` events
65
65
  of React Native's `Linking`, the routes run, the pending link opened after `setContext`. `pendingLinks.ts`: the one
66
- waiting link and its listeners, which `navigation` and `usePendingLink` read.
66
+ waiting link and its listeners, which `navigation` and `usePendingLink` read. `dispatchedLinks.ts` (iOS): hands
67
+ `Linking` the links of `ANN.DeepLinkDispatched`.
67
68
  - `events/` — `navigationEvents.ts`: the events by name, the native event behind each, `onNavigationEvent`,
68
69
  `listeners`. `screenLifecycle.ts`: what goes when a screen unmounts, `whenScreenUnmounts`. `tabPresses.ts`: the
69
70
  handlers of `useTabPress`.
@@ -268,7 +269,9 @@ its push, which the hook ignores.
268
269
  given once; JS asks only with `linking`) and `getStartInfo` (synchronous: `coldStart`, true for the whole life of
269
270
  the first JavaScript runtime of the process, and `splashLogo`, the size of the native splash's logo the CLI wrote,
270
271
  or null; `arrangements`, whether it shows pairs). The links that come while the app runs reach JS through React Native's
271
- `Linking`, not through `ANN.*` events.
272
+ `Linking`, not through `ANN.*` events. One exception, iOS only: a link the app dispatched (`dispatchDeepLinkURL`) that
273
+ `Linking` doesn't take from the native side (React Native 0.88 and newer, with a scene delegate) comes as
274
+ `ANN.DeepLinkDispatched`, and `src/api/linking/dispatchedLinks.ts` hands it to `Linking` as a `url` event.
272
275
  - Events: `ANN.AppLaunched`, `ANN.ComponentWillAppear`, `ANN.ComponentDidAppear`, `ANN.ComponentDidDisappear`,
273
276
  `ANN.NavigationButtonPressed`, `ANN.BottomTabSelected`, `ANN.BottomTabPressed`, `ANN.BottomTabLongPressed`,
274
277
  `ANN.ModalDismissed`, `ANN.ModalAttemptedToDismiss`, `ANN.ScreenPopped`, `ANN.SearchBarUpdated`,
@@ -276,8 +279,8 @@ its push, which the hook ignores.
276
279
  `ANN.NavigationStateChanged`, `ANN.SlotDestroyed`, `ANN.ScreenInsetsChanged`, `ANN.TitleRenamed`,
277
280
  `ANN.BottomAccessoryPlacementChanged`, `ANN.SheetDetentChanged`, `ANN.SplitViewDisplayModeChanged`,
278
281
  `ANN.KeyCommandPressed`, `ANN.NativeScreenCall`, `ANN.NativeScreenReleased`, `ANN.ScreenRegionsChanged`,
279
- `ANN.ArrangementChanged`, `ANN.HingeChanged`, `ANN.SplitViewColumnsStacked`. Their payloads are typed in
280
- `src/interfaces/ComponentEvents.ts` and `Events.ts`. Both platforms also send `ANN.CommandCompleted`
282
+ `ANN.ArrangementChanged`, `ANN.HingeChanged`, `ANN.SplitViewColumnsStacked`, `ANN.DeepLinkDispatched` (iOS). Their
283
+ payloads are typed in `src/interfaces/ComponentEvents.ts` and `Events.ts`. Both platforms also send `ANN.CommandCompleted`
281
284
  (`CommandCompletedEvent`), which nothing in JS listens to.
282
285
  - Native components: `ANNSlot` (`src/api/slots`) and `ANNSafeAreaView` (`src/api/safeArea`), with C++ of their own in
283
286
  `ios/cpp`, which Android builds too (their states hold a size and a padding; the first one comes from the props