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

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 (235) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -1
  3. package/android/CLAUDE.md +23 -14
  4. package/android/CORE.md +32 -19
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationActivity.kt +60 -13
  6. package/android/src/main/java/dev/apexnative/navigation/options/AnimationsOptions.kt +2 -2
  7. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabsOptions.kt +13 -8
  8. package/android/src/main/java/dev/apexnative/navigation/options/HardwareBackButtonOptions.kt +9 -1
  9. package/android/src/main/java/dev/apexnative/navigation/options/StackAnimationOptions.kt +40 -2
  10. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +28 -13
  11. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +7 -4
  12. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +3 -3
  13. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +39 -20
  14. package/android/src/main/java/dev/apexnative/navigation/react/ReloadCoordinator.kt +7 -6
  15. package/android/src/main/java/dev/apexnative/navigation/react/ScreenEnvironment.kt +177 -0
  16. package/android/src/main/java/dev/apexnative/navigation/react/ScreenRegions.kt +19 -51
  17. package/android/src/main/java/dev/apexnative/navigation/react/ScrollEvents.kt +49 -128
  18. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +30 -39
  19. package/android/src/main/java/dev/apexnative/navigation/react/events/EventEmitter.kt +34 -7
  20. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationState.kt +154 -0
  21. package/android/src/main/java/dev/apexnative/navigation/safearea/SafeAreaView.kt +12 -10
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +12 -12
  23. package/android/src/main/java/dev/apexnative/navigation/utils/ViewExtensions.kt +0 -13
  24. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/arrangement/ArrangementController.kt +10 -7
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsCollapseBehavior.kt +5 -3
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +22 -21
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowGeometry.kt +22 -0
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/TabsRoom.kt +0 -5
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ComponentViewController.kt +12 -9
  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/ScreenReadiness.kt +103 -56
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ScreenSafeArea.kt +29 -11
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/externalcomponent/ExternalComponentViewController.kt +2 -6
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +20 -2
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +119 -8
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +7 -0
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetBack.kt +42 -0
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackCheck.kt +50 -0
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/BackCallbackState.kt +45 -14
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +25 -5
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +2 -2
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/ParentController.kt +3 -6
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +3 -0
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/PreparedFrame.kt +3 -3
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +2 -2
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +13 -5
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +3 -0
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/ScreenSearch.kt +3 -5
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +2 -5
  50. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarComponents.kt +5 -51
  51. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +4 -9
  52. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ScrollEventListener.kt +6 -9
  53. package/android/src/main/java/dev/apexnative/navigation/views/animations/AnimationLayer.kt +73 -0
  54. package/android/src/main/java/dev/apexnative/navigation/views/animations/ValueInterpolation.kt +14 -0
  55. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabsLayout.kt +61 -2
  56. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomBottomTabItemView.kt +0 -5
  57. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/CustomTabRow.kt +72 -10
  58. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +9 -2
  59. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +11 -0
  60. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +318 -25
  61. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +18 -20
  62. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +17 -25
  63. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +15 -22
  64. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +12 -9
  65. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/TextChangeAnimator.kt +84 -35
  66. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +49 -40
  67. package/cli/doctor/checks/splash.js +4 -22
  68. package/cli/splash/index.js +3 -1
  69. package/docs-md/architecture/transitions.md +4 -2
  70. package/docs-md/containers/modals.md +4 -2
  71. package/docs-md/containers/overlays.md +1 -1
  72. package/docs-md/containers/side-menu.md +1 -1
  73. package/docs-md/containers/tabs.md +2 -0
  74. package/docs-md/flows/bottom-sheets.md +1 -1
  75. package/docs-md/fundamentals/starting-the-app.md +1 -1
  76. package/docs-md/guides/cli.md +1 -2
  77. package/docs-md/help/glossary.md +2 -2
  78. package/docs-md/help/troubleshooting.md +23 -2
  79. package/docs-md/index.json +12 -0
  80. package/docs-md/migration/from-expo-router.md +1 -1
  81. package/docs-md/migration/from-react-native-navigation.md +1 -1
  82. package/docs-md/migration/from-react-navigation.md +2 -2
  83. package/docs-md/screen-ui/back-and-gestures.md +36 -0
  84. package/docs-md/screen-ui/bar-buttons.md +4 -4
  85. package/docs-md/screen-ui/search.md +1 -1
  86. package/docs-md/screen-ui/system-ui.md +16 -11
  87. package/docs-md/screen-ui/toolbar-and-fab.md +2 -2
  88. package/docs-md/screen-ui/top-bar.md +8 -4
  89. package/docs-md/start/installation.md +1 -1
  90. package/ios/Animations/ANNFrameDriver.mm +2 -0
  91. package/ios/Animations/Elements/ANNSharedElementFlight.mm +70 -4
  92. package/ios/Animations/Interpolators/ANNInterpolate.h +2 -30
  93. package/ios/Animations/Interpolators/ANNTransformParts.h +48 -0
  94. package/ios/Animations/Interpolators/ANNTransformParts.mm +507 -0
  95. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -4
  96. package/ios/Animations/Interpolators/ANNViewTracks.mm +49 -9
  97. package/ios/CLAUDE.md +17 -15
  98. package/ios/CORE.md +30 -26
  99. package/ios/Contract/ANNCommandAnswer.h +9 -2
  100. package/ios/Contract/ANNCommandAnswer.mm +25 -12
  101. package/ios/Contract/ANNCommandRunner.h +8 -7
  102. package/ios/Contract/ANNCommandRunner.mm +21 -27
  103. package/ios/Contract/ANNLayoutNode.h +3 -3
  104. package/ios/Contract/ANNLayoutReader.h +3 -3
  105. package/ios/Core/ANNAppearanceDispatcher.h +5 -4
  106. package/ios/Core/ANNAppearanceDispatcher.mm +12 -1
  107. package/ios/Core/ANNJavaScriptCalls.h +11 -12
  108. package/ios/Core/ANNJavaScriptCalls.mm +7 -16
  109. package/ios/Core/ANNNavigationState.h +4 -6
  110. package/ios/Core/ANNNavigationState.mm +0 -4
  111. package/ios/Core/ANNReloadCoordinator.mm +4 -8
  112. package/ios/Core/ApexNavigation+Internal.h +1 -1
  113. package/ios/Layouts/Arrangement/ANNArrangementState.h +2 -17
  114. package/ios/Layouts/Arrangement/ANNArrangementState.mm +0 -38
  115. package/ios/Layouts/Arrangement/ANNArrangementViewController.mm +4 -4
  116. package/ios/Layouts/Root/ANNSplashViewController.h +5 -3
  117. package/ios/Layouts/Root/ANNSplashViewController.mm +147 -0
  118. package/ios/Layouts/Screen/ANNComponentViewController.h +2 -2
  119. package/ios/Layouts/Screen/ANNComponentViewController.mm +9 -22
  120. package/ios/Layouts/Screen/ANNPreviewInteraction.h +7 -4
  121. package/ios/Layouts/Screen/ANNPreviewInteraction.mm +72 -10
  122. package/ios/Layouts/Screen/ANNScreenEnvironment.h +63 -0
  123. package/ios/Layouts/Screen/ANNScreenEnvironment.mm +181 -0
  124. package/ios/Layouts/Screen/ANNScreenRegions.h +3 -18
  125. package/ios/Layouts/Screen/ANNScreenRegions.mm +0 -38
  126. package/ios/Layouts/Screen/ANNScreenSafeArea.h +4 -9
  127. package/ios/Layouts/Screen/ANNScreenSafeArea.mm +14 -16
  128. package/ios/Layouts/Stack/ANNPreviewPush.h +7 -6
  129. package/ios/Layouts/Stack/ANNPreviewPush.mm +142 -27
  130. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.h +12 -1
  131. package/ios/Layouts/Tabs/ANNBottomTabsAccessoryView.mm +23 -0
  132. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +2 -1
  133. package/ios/Layouts/Tabs/ANNBottomTabsCustomRow.mm +8 -3
  134. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +4 -0
  135. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +123 -36
  136. package/ios/Layouts/Tabs/ANNTabsDesign.h +6 -5
  137. package/ios/Layouts/Tabs/ANNTabsDesign.mm +9 -1
  138. package/ios/Layouts/Tabs/UITabBar+utils.h +5 -5
  139. package/ios/Layouts/Tabs/UITabBar+utils.mm +32 -13
  140. package/ios/Options/ANNBottomTabsOptions.h +2 -0
  141. package/ios/Options/ANNBottomTabsOptions.mm +1 -0
  142. package/ios/Options/ANNOptions.h +18 -0
  143. package/ios/Options/ANNOptions.mm +112 -1
  144. package/ios/Options/ANNOptionsReader.mm +110 -0
  145. package/ios/Options/Params/ANNParam.h +15 -0
  146. package/ios/Options/Params/ANNParam.mm +61 -0
  147. package/ios/Options/Params/Color.h +3 -0
  148. package/ios/Options/Params/Color.mm +21 -0
  149. package/ios/Options/Params/Image.h +4 -0
  150. package/ios/Options/Params/Image.mm +20 -0
  151. package/ios/React/ANNReactView.mm +6 -25
  152. package/ios/React/ANNSafeAreaViewComponentView.mm +26 -15
  153. package/ios/React/ANNSurfaceHostingView.h +6 -0
  154. package/ios/React/ANNSurfaceHostingView.mm +116 -0
  155. package/ios/TopBar/ANNBackButtonPresenter.mm +6 -5
  156. package/ios/TopBar/ANNBarAppearance.h +2 -1
  157. package/ios/TopBar/ANNBarAppearance.mm +111 -30
  158. package/ios/TopBar/ANNBarBackgroundComponent.mm +16 -0
  159. package/ios/TopBar/ANNBarButtonComponentView.h +4 -4
  160. package/ios/TopBar/ANNBarButtonComponentView.mm +1 -1
  161. package/ios/TopBar/ANNBarButtons.h +5 -3
  162. package/ios/TopBar/ANNBarButtons.mm +186 -17
  163. package/ios/TopBar/ANNLargeTitlePresenter.mm +24 -4
  164. package/ios/TopBar/ANNMainList.h +3 -0
  165. package/ios/TopBar/ANNMainList.mm +4 -0
  166. package/ios/TopBar/ANNScrollEdgeEffects.h +22 -6
  167. package/ios/TopBar/ANNScrollEdgeEffects.mm +123 -16
  168. package/ios/TopBar/ANNSearchPresenter.mm +2 -2
  169. package/ios/TopBar/ANNSearchResultsViewController.mm +5 -5
  170. package/ios/TopBar/ANNTopBarPresenter.h +3 -2
  171. package/ios/TopBar/ANNTopBarPresenter.mm +95 -22
  172. package/ios/TurboModules/ANNTurboModule.mm +20 -16
  173. package/lib/module/ARCHITECTURE.md +8 -4
  174. package/lib/module/api/commands/commands.js +54 -6
  175. package/lib/module/api/commands/commands.js.map +1 -1
  176. package/lib/module/api/hooks/useSafeAreaInsets.js +4 -3
  177. package/lib/module/api/hooks/useSafeAreaInsets.js.map +1 -1
  178. package/lib/module/api/options/OptionsChain.js +2 -2
  179. package/lib/module/engine/commands.js +21 -1
  180. package/lib/module/engine/commands.js.map +1 -1
  181. package/lib/module/engine/places.js +8 -0
  182. package/lib/module/engine/places.js.map +1 -1
  183. package/lib/module/engine/screens.js +29 -4
  184. package/lib/module/engine/screens.js.map +1 -1
  185. package/lib/typescript/api/app/createNavigation.d.ts +3 -3
  186. package/lib/typescript/api/commands/commands.d.ts.map +1 -1
  187. package/lib/typescript/api/commands/presentation.d.ts +4 -3
  188. package/lib/typescript/api/commands/presentation.d.ts.map +1 -1
  189. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts +4 -3
  190. package/lib/typescript/api/hooks/useSafeAreaInsets.d.ts.map +1 -1
  191. package/lib/typescript/api/options/OptionsChain.d.ts +2 -2
  192. package/lib/typescript/engine/commands.d.ts.map +1 -1
  193. package/lib/typescript/engine/places.d.ts +2 -0
  194. package/lib/typescript/engine/places.d.ts.map +1 -1
  195. package/lib/typescript/engine/screens.d.ts +20 -6
  196. package/lib/typescript/engine/screens.d.ts.map +1 -1
  197. package/lib/typescript/interfaces/Options.d.ts +1 -1
  198. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  199. package/lib/typescript/interfaces/options/animations.d.ts +30 -9
  200. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  201. package/lib/typescript/interfaces/options/bottomTabs.d.ts +18 -5
  202. package/lib/typescript/interfaces/options/bottomTabs.d.ts.map +1 -1
  203. package/lib/typescript/interfaces/options/buttons.d.ts +8 -8
  204. package/lib/typescript/interfaces/options/stack.d.ts +9 -0
  205. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  206. package/lib/typescript/interfaces/options/system.d.ts +8 -5
  207. package/lib/typescript/interfaces/options/system.d.ts.map +1 -1
  208. package/lib/typescript/interfaces/options/toolbar.d.ts +2 -2
  209. package/lib/typescript/interfaces/options/topBar.d.ts +32 -29
  210. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  211. package/package.json +1 -1
  212. package/src/ARCHITECTURE.md +8 -4
  213. package/src/api/app/createNavigation.ts +3 -3
  214. package/src/api/commands/commands.ts +48 -7
  215. package/src/api/commands/presentation.ts +4 -3
  216. package/src/api/hooks/useSafeAreaInsets.ts +4 -3
  217. package/src/api/options/OptionsChain.ts +2 -2
  218. package/src/engine/commands.ts +30 -6
  219. package/src/engine/places.ts +9 -0
  220. package/src/engine/screens.tsx +29 -8
  221. package/src/interfaces/Options.ts +1 -0
  222. package/src/interfaces/options/animations.ts +31 -9
  223. package/src/interfaces/options/bottomTabs.ts +18 -5
  224. package/src/interfaces/options/buttons.ts +8 -8
  225. package/src/interfaces/options/stack.ts +9 -0
  226. package/src/interfaces/options/system.ts +8 -5
  227. package/src/interfaces/options/toolbar.ts +2 -2
  228. package/src/interfaces/options/topBar.ts +32 -29
  229. package/android/src/main/java/dev/apexnative/navigation/react/ArrangementStates.kt +0 -42
  230. package/android/src/main/java/dev/apexnative/navigation/react/ScreenInsets.kt +0 -70
  231. package/android/src/main/java/dev/apexnative/navigation/react/events/NavigationStateReporter.kt +0 -62
  232. package/android/src/main/java/dev/apexnative/navigation/react/events/StateHold.kt +0 -62
  233. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/CustomTabRowPlacer.kt +0 -223
  234. package/ios/Layouts/Screen/ANNScreenInsets.h +0 -30
  235. package/ios/Layouts/Screen/ANNScreenInsets.mm +0 -70
@@ -1,5 +1,4 @@
1
1
  #import "ANNReloadCoordinator.h"
2
- #import "ANNArrangementState.h"
3
2
  #import "ANNAssert.h"
4
3
  #import "ANNErrorTexts.h"
5
4
  #import "ANNHinge.h"
@@ -12,9 +11,8 @@
12
11
  #import "ANNPreviewInteraction.h"
13
12
  #import "ANNPreviewPush.h"
14
13
  #import "ANNRootCoordinator.h"
15
- #import "ANNScreenInsets.h"
14
+ #import "ANNScreenEnvironment.h"
16
15
  #import "ANNScreenReadiness.h"
17
- #import "ANNScreenRegions.h"
18
16
  #import "ANNSplashViewController.h"
19
17
  #import "ANNSplitViewCommands.h"
20
18
  #import "ANNTabsPreloadWindow.h"
@@ -154,12 +152,10 @@ static BOOL ANNWatchingBundles;
154
152
  [oldScreens addObjectsFromArray:[ANNSplitViewCommands takeAllForReload]];
155
153
  // The new JavaScript counts its ids anew: nothing the safe area views of the old screens said stays ready for a new
156
154
  // screen with the same id. The old surfaces are stopped or silent. The same for what the old screens kept for
157
- // JavaScript: their insets and padding, regions and arrangement states (an old screen still in a window writes no
158
- // insets or regions again: its emitter is not current).
155
+ // JavaScript, their environment: insets and padding, regions and arrangements (an old screen still in a window
156
+ // writes no insets or regions again: its emitter is not current).
159
157
  [ANNScreenReadiness reset];
160
- [ANNScreenInsets removeAll];
161
- [ANNScreenRegions removeAll];
162
- [ANNArrangementState removeAll];
158
+ [ANNScreenEnvironment removeAll];
163
159
  // 6. The window shows the blank screen, which counts as the splash: a reload is not a launch. The
164
160
  // reset runs from the second bundle of the process on.
165
161
  if (window) {
@@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN
18
18
  + (nullable ANNNativeScreenCreator)nativeScreenCreatorForName:(NSString *)name;
19
19
 
20
20
  /// Whether a native screen is registered under the name by `registerNativeScreen:creator:` (the registration of 8.x is
21
- /// gone). Any thread: the layout of a command is checked on the module's queue.
21
+ /// gone). Any thread (the layout check of a command reads it on the main queue).
22
22
  + (BOOL)hasNativeScreenNamed:(NSString *)name;
23
23
 
24
24
  @end
@@ -1,30 +1,15 @@
1
- #import "ANNEventEmitter.h"
2
1
  #import <UIKit/UIKit.h>
3
2
 
4
3
  NS_ASSUME_NONNULL_BEGIN
5
4
 
6
- /// The state of each screen of an arrangement (`useArrangement`): `{ placement, layout, front, hidden }`, which view of
7
- /// the pair it is and how the pair lies. The native side keeps it on the main queue; JavaScript reads it from its own
8
- /// thread for a screen's first render, and gets every change as `ANN.ArrangementChanged`.
5
+ /// Whether the app can show pairs at all. The state of each screen of a pair (`useArrangement`) is kept for JavaScript
6
+ /// by `ANNScreenEnvironment`.
9
7
  @interface ANNArrangementState : NSObject
10
8
 
11
9
  /// Whether the app shows arrangements (`getStartInfo().arrangements`): built with the iOS 27.1 SDK and running on
12
10
  /// iOS 27.1 or later. Without them JavaScript sends only the primary, as a screen. Any thread.
13
11
  + (BOOL)canShowArrangements;
14
12
 
15
- /// Keeps the screen's state and tells when it changed; nothing goes without an emitter. Main queue.
16
- + (void)setState:(NSDictionary<NSString *, id> *)state
17
- forScreenId:(NSString *)screenId
18
- eventEmitter:(nullable ANNEventEmitter *)eventEmitter;
19
-
20
- /// The screen's state, or nil for a screen outside a pair. Any thread.
21
- + (nullable NSDictionary<NSString *, id> *)stateForScreenId:(NSString *)screenId;
22
-
23
- + (void)removeScreenId:(NSString *)screenId;
24
-
25
- /// Forgets every screen: after a reload the new JavaScript counts its ids anew. Main queue.
26
- + (void)removeAll;
27
-
28
13
  @end
29
14
 
30
15
  NS_ASSUME_NONNULL_END
@@ -1,16 +1,8 @@
1
1
  #import "ANNArrangementState.h"
2
2
  #import "ANNArrangementViewController.h"
3
3
 
4
- static NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *ANNArrangementStateById;
5
-
6
4
  @implementation ANNArrangementState
7
5
 
8
- + (void)initialize {
9
- if (self == ANNArrangementState.class) {
10
- ANNArrangementStateById = [NSMutableDictionary new];
11
- }
12
- }
13
-
14
6
  + (BOOL)canShowArrangements {
15
7
  #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 270100
16
8
  if (@available(iOS 27.1, *)) {
@@ -20,34 +12,4 @@ static NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *ANNArran
20
12
  return NO;
21
13
  }
22
14
 
23
- + (void)setState:(NSDictionary<NSString *, id> *)state
24
- forScreenId:(NSString *)screenId
25
- eventEmitter:(ANNEventEmitter *)eventEmitter {
26
- @synchronized(ANNArrangementStateById) {
27
- if ([ANNArrangementStateById[screenId] isEqualToDictionary:state]) {
28
- return;
29
- }
30
- ANNArrangementStateById[screenId] = state;
31
- }
32
- [eventEmitter sendArrangementChanged:screenId state:state];
33
- }
34
-
35
- + (NSDictionary<NSString *, id> *)stateForScreenId:(NSString *)screenId {
36
- @synchronized(ANNArrangementStateById) {
37
- return ANNArrangementStateById[screenId];
38
- }
39
- }
40
-
41
- + (void)removeScreenId:(NSString *)screenId {
42
- @synchronized(ANNArrangementStateById) {
43
- [ANNArrangementStateById removeObjectForKey:screenId];
44
- }
45
- }
46
-
47
- + (void)removeAll {
48
- @synchronized(ANNArrangementStateById) {
49
- [ANNArrangementStateById removeAllObjects];
50
- }
51
- }
52
-
53
15
  @end
@@ -2,10 +2,10 @@
2
2
 
3
3
  #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 270100
4
4
 
5
- #import "ANNArrangementState.h"
6
5
  #import "ANNComponentViewController.h"
7
6
  #import "ANNEventContract.h"
8
7
  #import "ANNExternalViewController.h"
8
+ #import "ANNScreenEnvironment.h"
9
9
  #import "ANNShownScreens.h"
10
10
  #import "ANNUtils.h"
11
11
 
@@ -298,9 +298,9 @@ static BOOL ANNIsShown(UIViewController *screen, UIArrangementViewState *state,
298
298
  ofScreen:(UIViewController *)screen
299
299
  eventEmitter:(ANNEventEmitter *)eventEmitter {
300
300
  if (ANNKeepsState(screen)) {
301
- [ANNArrangementState setState:state
302
- forScreenId:screen.layoutInfo.componentId
303
- eventEmitter:eventEmitter];
301
+ [ANNScreenEnvironment setArrangement:state
302
+ forScreenId:screen.layoutInfo.componentId
303
+ eventEmitter:eventEmitter];
304
304
  }
305
305
  }
306
306
 
@@ -7,9 +7,11 @@ NS_ASSUME_NONNULL_BEGIN
7
7
  /// either at once. The status bar follows the Info.plist keys `UIStatusBarStyle` and `UIStatusBarHidden`. Main queue.
8
8
  @interface ANNSplashViewController : UIViewController
9
9
 
10
- /// The copy of the launch screen: the initial view controller of `UILaunchStoryboardName` as a child (a storyboard
11
- /// that doesn't load: the first view of the nib of that name), else the legacy launch images, full screen. Nil when the
12
- /// app has none of these: the window then has no root view controller until the first root.
10
+ /// The copy of the launch screen. When iOS builds it from the Info.plist (`UILaunchScreen`, or the default of
11
+ /// `UILaunchScreens`), the same color, image and bars as plain views; else the initial view controller of
12
+ /// `UILaunchStoryboardName` as a child (a storyboard that doesn't load: the first view of the nib of that name), else
13
+ /// the legacy launch images, full screen. Nil when the app has none of these: the window then has no root view
14
+ /// controller until the first root.
13
15
  + (nullable instancetype)launchScreenCopyWithBounds:(CGRect)bounds;
14
16
 
15
17
  /// The blank screen after a reload: clear when the default options give the window a color (`window.backgroundColor`),
@@ -34,9 +34,63 @@ static NSArray<NSString *> *ANNLegacyLaunchImageNames(CGFloat height) {
34
34
  return names;
35
35
  }
36
36
 
37
+ static NSDictionary *ANNDictionaryOrNil(id value) {
38
+ return [value isKindOfClass:NSDictionary.class] ? value : nil;
39
+ }
40
+
41
+ static NSString *ANNStringOrNil(id value) {
42
+ return [value isKindOfClass:NSString.class] && [value length] > 0 ? value : nil;
43
+ }
44
+
45
+ // The definition that a plural key (`UILaunchScreens`, `UILaunchStoryboards`) marks as its default.
46
+ static NSDictionary *ANNDefaultDefinition(id plural, NSString *definitionsKey,
47
+ NSString *identifierKey, NSString *defaultKey) {
48
+ NSDictionary *dictionary = ANNDictionaryOrNil(plural);
49
+ NSArray *definitions = dictionary[definitionsKey];
50
+ NSString *defaultIdentifier = ANNStringOrNil(dictionary[defaultKey]);
51
+ if (![definitions isKindOfClass:NSArray.class] || !defaultIdentifier) {
52
+ return nil;
53
+ }
54
+ for (id definition in definitions) {
55
+ NSDictionary *entry = ANNDictionaryOrNil(definition);
56
+ if ([entry[identifierKey] isEqual:defaultIdentifier]) {
57
+ return entry;
58
+ }
59
+ }
60
+ return nil;
61
+ }
62
+
63
+ // The launch screen that iOS builds from the Info.plist without a storyboard, or nil when it shows a storyboard. Of
64
+ // `UILaunchStoryboardName`, `UILaunchScreen`, the default of `UILaunchStoryboards` and the default of
65
+ // `UILaunchScreens`, the last one present wins (SplashBoard's `XBApplicationLaunchCompatibilityInfo`). A launch by a
66
+ // link that `UILaunchScreens` maps to a screen of its own still gets the default here.
67
+ static NSDictionary *ANNLaunchScreenConfiguration(NSBundle *bundle) {
68
+ NSDictionary *screen =
69
+ ANNDefaultDefinition([bundle objectForInfoDictionaryKey:@"UILaunchScreens"],
70
+ @"UILaunchScreenDefinitions", @"UILaunchScreenIdentifier",
71
+ @"UIDefaultLaunchScreen");
72
+ if (screen) {
73
+ return screen;
74
+ }
75
+ if (ANNDefaultDefinition([bundle objectForInfoDictionaryKey:@"UILaunchStoryboards"],
76
+ @"UILaunchStoryboardDefinitions", @"UILaunchStoryboardIdentifier",
77
+ @"UIDefaultLaunchStoryboard")) {
78
+ return nil;
79
+ }
80
+ return ANNDictionaryOrNil([bundle objectForInfoDictionaryKey:@"UILaunchScreen"]);
81
+ }
82
+
37
83
  @implementation ANNSplashViewController {
38
84
  UIViewController *_content;
39
85
  UIView *_contentView;
86
+ NSDictionary *_launchScreen;
87
+ // The parts of a launch screen built from `UILaunchScreen`, laid out once the safe area is known.
88
+ UIImageView *_imageView;
89
+ BOOL _imageRespectsSafeArea;
90
+ UINavigationBar *_navigationBar;
91
+ UIImageView *_navigationBarImageView;
92
+ UIView *_bottomBar;
93
+ UIImageView *_bottomBarImageView;
40
94
  }
41
95
 
42
96
  - (instancetype)initWithContent:(UIViewController *)content
@@ -52,6 +106,12 @@ static NSArray<NSString *> *ANNLegacyLaunchImageNames(CGFloat height) {
52
106
 
53
107
  + (instancetype)launchScreenCopyWithBounds:(CGRect)bounds {
54
108
  NSBundle *bundle = NSBundle.mainBundle;
109
+ NSDictionary *launchScreen = ANNLaunchScreenConfiguration(bundle);
110
+ if (launchScreen) {
111
+ ANNSplashViewController *copy = [[self alloc] initWithContent:nil contentView:nil blank:NO];
112
+ copy->_launchScreen = launchScreen;
113
+ return copy;
114
+ }
55
115
  NSString *storyboardName = [bundle objectForInfoDictionaryKey:@"UILaunchStoryboardName"];
56
116
  if ([storyboardName isKindOfClass:NSString.class] && storyboardName.length > 0) {
57
117
  UIViewController *initial = nil;
@@ -97,6 +157,10 @@ static NSArray<NSString *> *ANNLegacyLaunchImageNames(CGFloat height) {
97
157
  view.backgroundColor = windowHasColor ? UIColor.clearColor : UIColor.systemBackgroundColor;
98
158
  return;
99
159
  }
160
+ if (_launchScreen) {
161
+ [self buildLaunchScreen];
162
+ return;
163
+ }
100
164
  UIView *contentView = _contentView;
101
165
  if (_content) {
102
166
  [self addChildViewController:_content];
@@ -109,6 +173,89 @@ static NSArray<NSString *> *ANNLegacyLaunchImageNames(CGFloat height) {
109
173
  [_content didMoveToParentViewController:self];
110
174
  }
111
175
 
176
+ #pragma mark - The launch screen of `UILaunchScreen`
177
+
178
+ // Built as SplashBoard builds it: the named color over the whole screen, else the system background; the named image
179
+ // at its own size in the middle of the screen or of its safe area, stretched instead when the asset has slicing; a tab
180
+ // bar or a toolbar at the bottom and a navigation bar at the top, the system's own bars, or the bar's named image
181
+ // stretched over the bar's place. A name the asset catalog doesn't have leaves its part out, as iOS does.
182
+ - (void)buildLaunchScreen {
183
+ UIView *view = self.view;
184
+ NSString *colorName = ANNStringOrNil(_launchScreen[@"UIColorName"]);
185
+ UIColor *color = colorName ? [UIColor colorNamed:colorName] : nil;
186
+ view.backgroundColor = color ?: UIColor.systemBackgroundColor;
187
+
188
+ NSString *imageName = ANNStringOrNil(_launchScreen[@"UIImageName"]);
189
+ UIImage *image = imageName ? [UIImage imageNamed:imageName] : nil;
190
+ if (image) {
191
+ _imageView = [[UIImageView alloc] initWithImage:image];
192
+ _imageView.contentMode = UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero)
193
+ ? UIViewContentModeCenter
194
+ : UIViewContentModeScaleToFill;
195
+ id respects = _launchScreen[@"UIImageRespectsSafeAreaInsets"];
196
+ _imageRespectsSafeArea = [respects respondsToSelector:@selector(boolValue)] &&
197
+ [respects boolValue];
198
+ [view addSubview:_imageView];
199
+ }
200
+
201
+ // A toolbar takes the bottom before a tab bar; a navigation bar comes on top of either.
202
+ id toolbar = _launchScreen[@"UIToolbar"];
203
+ id tabBar = _launchScreen[@"UITabBar"];
204
+ if (toolbar || tabBar) {
205
+ _bottomBar = toolbar ? [UIToolbar new] : [UITabBar new];
206
+ _bottomBarImageView = [self addBar:_bottomBar configuration:toolbar ?: tabBar];
207
+ }
208
+ id navigationBar = _launchScreen[@"UINavigationBar"];
209
+ if (navigationBar) {
210
+ _navigationBar = [UINavigationBar new];
211
+ _navigationBarImageView = [self addBar:_navigationBar configuration:navigationBar];
212
+ }
213
+ }
214
+
215
+ // Adds the bar, or its image in its place when its configuration names one that loads.
216
+ - (UIImageView *)addBar:(UIView *)bar configuration:(id)configuration {
217
+ NSString *name = ANNStringOrNil(ANNDictionaryOrNil(configuration)[@"UIImageName"]);
218
+ UIImage *image = name ? [UIImage imageNamed:name] : nil;
219
+ UIImageView *imageView = image ? [[UIImageView alloc] initWithImage:image] : nil;
220
+ bar.hidden = imageView != nil;
221
+ [self.view addSubview:bar];
222
+ if (imageView) {
223
+ [self.view addSubview:imageView];
224
+ }
225
+ return imageView;
226
+ }
227
+
228
+ // The safe area is known only in the window, so the parts are laid out here, after every change of it.
229
+ - (void)viewDidLayoutSubviews {
230
+ [super viewDidLayoutSubviews];
231
+ if (!_launchScreen) {
232
+ return;
233
+ }
234
+ CGRect bounds = self.view.bounds;
235
+ UIEdgeInsets safeArea = self.view.safeAreaInsets;
236
+ _imageView.frame =
237
+ _imageRespectsSafeArea ? UIEdgeInsetsInsetRect(bounds, safeArea) : bounds;
238
+ [self layOutBar:_bottomBar imageView:_bottomBarImageView atBottom:YES];
239
+ [self layOutBar:_navigationBar imageView:_navigationBarImageView atBottom:NO];
240
+ }
241
+
242
+ // A bar is as tall as UIKit makes it, plus the safe area on its edge of the screen, and as wide as the screen.
243
+ - (void)layOutBar:(UIView *)bar
244
+ imageView:(UIImageView *)imageView
245
+ atBottom:(BOOL)atBottom {
246
+ if (!bar) {
247
+ return;
248
+ }
249
+ CGRect bounds = self.view.bounds;
250
+ UIEdgeInsets safeArea = self.view.safeAreaInsets;
251
+ CGFloat height = [bar sizeThatFits:bounds.size].height +
252
+ (atBottom ? safeArea.bottom : safeArea.top);
253
+ CGFloat y = atBottom ? CGRectGetMaxY(bounds) - height : CGRectGetMinY(bounds);
254
+ CGRect frame = CGRectMake(CGRectGetMinX(bounds), y, CGRectGetWidth(bounds), height);
255
+ bar.frame = frame;
256
+ imageView.frame = frame;
257
+ }
258
+
112
259
  #pragma mark - The status bar, as the launch screen had it
113
260
 
114
261
  - (UIStatusBarStyle)preferredStatusBarStyle {
@@ -11,8 +11,8 @@ NS_ASSUME_NONNULL_BEGIN
11
11
  ///
12
12
  /// Its view has the size of the screen of the navigation window's scene; the content sits in the root frame, by the one
13
13
  /// "under bars" rule (`ANNScreenBars`). Every recompute of the frame (a layout, a change of the safe area or the
14
- /// traits, `mergeOptions`, an appearance) gives the modules the screen's insets, padding and regions
15
- /// (`ANNScreenInsets`, `ANNScreenRegions`). Its presenter is an `ANNComponentPresenter`.
14
+ /// traits, `mergeOptions`, an appearance) gives the store the screen's insets, padding and regions
15
+ /// (`ANNScreenEnvironment`). Its presenter is an `ANNComponentPresenter`.
16
16
  ///
17
17
  /// The screen answers UIKit's questions about the system UI (native screens too): the status bar's style and visibility
18
18
  /// (never animated), the orientations, the home indicator, the deferred edges and the vertical bars, from its resolved
@@ -1,9 +1,7 @@
1
1
  #import "ANNComponentViewController.h"
2
2
  #import "ANNAppearanceDispatcher.h"
3
- #import "ANNArrangementState.h"
4
3
  #import "ANNBottomTabsController.h"
5
4
  #import "ANNComponentPresenter.h"
6
- #import "ANNEventContract.h"
7
5
  #import "ANNHinge.h"
8
6
  #import "ANNKeyCommands.h"
9
7
  #import "ANNNodeRegistry.h"
@@ -11,7 +9,7 @@
11
9
  #import "ANNPendingLayers.h"
12
10
  #import "ANNPreviewPush.h"
13
11
  #import "ANNScreenBars.h"
14
- #import "ANNScreenInsets.h"
12
+ #import "ANNScreenEnvironment.h"
15
13
  #import "ANNScreenReadiness.h"
16
14
  #import "ANNScreenRegions.h"
17
15
  #import "ANNScreenSafeArea.h"
@@ -432,7 +430,7 @@
432
430
  return UIRectEdgeNone;
433
431
  }
434
432
 
435
- // The insets and the padding of a React screen or a slot screen go to the module, which keeps them, gives JavaScript
433
+ // The insets and the padding of a React screen or a slot screen go to the store, which keeps them, gives JavaScript
436
434
  // what still covers the content and tells only of changes; the regions in the same pass. Only in a window and with an
437
435
  // id; never after the destroy, which removed them (a late layout would bring them back), nor for the JavaScript that
438
436
  // went (an old screen held after a reload: the new JavaScript may give its id to a new screen).
@@ -453,22 +451,18 @@
453
451
  UINavigationController *stack = [self shownNavigation];
454
452
  ANNScreenPaddingContext context = {
455
453
  .inStack = stack != nil,
456
- // The stack's toolbar, by the rule the stack shows it with: one that `topBar.hideOnScroll`
457
- // hid with the bar still counts.
458
- .stackShowsToolbar = stack != nil && options.toolbar.shouldShow,
459
- .windowBottomSafeArea = window.safeAreaInsets.bottom,
460
454
  .verticalBarSide = [self verticalBarSide],
461
455
  };
462
456
  UIEdgeInsets padding = ANNScreenPadding(insets, options, context);
463
- [ANNScreenInsets setInsets:insets
464
- padding:padding
465
- forScreenId:componentId
466
- eventEmitter:eventEmitter];
457
+ [ANNScreenEnvironment setInsets:insets
458
+ padding:padding
459
+ forScreenId:componentId
460
+ eventEmitter:eventEmitter];
467
461
  [self updateRegions];
468
462
  }
469
463
 
470
464
  // The regions in the same pass, and on the next turn after a change of the hinge's state (`hingeDidChange:`). Nil
471
- // before iOS 27.1: nothing to keep. An empty first result is not sent.
465
+ // before iOS 27.1: nothing to keep. The store neither keeps nor sends an empty first result.
472
466
  - (void)updateRegions {
473
467
  UIView<ANNComponentProtocol> *content = _reactView;
474
468
  NSString *componentId = self.layoutInfo.componentId;
@@ -483,12 +477,7 @@
483
477
  if (!regions) {
484
478
  return;
485
479
  }
486
- const auto &keys = ANNEventContract.ScreenRegionsChanged;
487
- BOOL empty = [regions[keys.division] count] == 0 && [regions[keys.occlusion] count] == 0;
488
- if (empty && ![ANNScreenRegions regionsForScreenId:componentId]) {
489
- return;
490
- }
491
- [ANNScreenRegions setRegions:regions forScreenId:componentId eventEmitter:eventEmitter];
480
+ [ANNScreenEnvironment setRegions:regions forScreenId:componentId eventEmitter:eventEmitter];
492
481
  }
493
482
 
494
483
  - (void)hingeDidChange:(NSNotification *)notification {
@@ -690,9 +679,7 @@
690
679
  // its destroy, as at a reload) keeps it.
691
680
  NSString *componentId = self.layoutInfo.componentId;
692
681
  if (componentId && ![ANNNodeRegistry nodeForComponentId:componentId]) {
693
- [ANNScreenInsets removeScreenId:componentId];
694
- [ANNScreenRegions removeScreenId:componentId];
695
- [ANNArrangementState removeScreenId:componentId];
682
+ [ANNScreenEnvironment removeScreenId:componentId];
696
683
  [ANNScreenReadiness removeScreenId:componentId];
697
684
  }
698
685
  }
@@ -9,15 +9,18 @@
9
9
 
10
10
  /**
11
11
  * Previews `preview` from `view`, the component view with the tag `reactTag`, and replaces an earlier
12
- * preview of the view. `commit` runs on a tap on the preview and returns whether it showed the
13
- * screen; the preview goes then, as the push it stands for is done. `dismissed` runs once when the
14
- * preview goes without being shown as a screen: replaced by another preview of the view, after
15
- * Fabric gave the view to another component, or when `source`, the screen of the view, goes.
12
+ * preview of the view. `prepare` runs as each press starts, before the menu shows the preview: it
13
+ * renders the screen if it hasn't yet, and returns NO when the preview went instead (no menu shows
14
+ * then). `commit` runs on a tap on the preview and returns whether it showed the screen; the
15
+ * preview goes then, as the push it stands for is done. `dismissed` runs once when the preview
16
+ * goes without being shown as a screen: replaced by another preview of the view, after Fabric gave
17
+ * the view to another component, or when `source`, the screen of the view, goes.
16
18
  */
17
19
  + (void)attachPreview:(UIViewController *)preview
18
20
  toView:(UIView *)view
19
21
  reactTag:(NSInteger)reactTag
20
22
  source:(UIViewController *)source
23
+ prepare:(BOOL (^)(void))prepare
21
24
  commit:(BOOL (^)(void))commit
22
25
  dismissed:(void (^)(void))dismissed;
23
26
 
@@ -24,15 +24,23 @@ static NSMutableSet *ANNPreviews(void) {
24
24
  @implementation ANNPreviewInteraction {
25
25
  UIViewController *_preview;
26
26
  NSInteger _reactTag;
27
+ __weak UIViewController *_source;
28
+ BOOL (^_prepare)(void);
27
29
  BOOL (^_commit)(void);
28
30
  void (^_dismissed)(void);
29
31
  UIContextMenuInteraction *_interaction;
32
+ // The snapshots of the view that the menu of this press lifts and puts back, over the view until
33
+ // the menu is gone. Each press has its own list: a new press may start while the last menu goes.
34
+ NSMutableArray<UIView *> *_snapshots;
35
+ // The list of the menu that goes now: its end removes them.
36
+ __weak NSMutableArray<UIView *> *_endingSnapshots;
30
37
  }
31
38
 
32
39
  + (void)attachPreview:(UIViewController *)preview
33
40
  toView:(UIView *)view
34
41
  reactTag:(NSInteger)reactTag
35
42
  source:(UIViewController *)source
43
+ prepare:(BOOL (^)(void))prepare
36
44
  commit:(BOOL (^)(void))commit
37
45
  dismissed:(void (^)(void))dismissed {
38
46
  if (!view || !preview) {
@@ -45,6 +53,8 @@ static NSMutableSet *ANNPreviews(void) {
45
53
  ANNPreviewInteraction *previewInteraction = [ANNPreviewInteraction new];
46
54
  previewInteraction->_preview = preview;
47
55
  previewInteraction->_reactTag = reactTag;
56
+ previewInteraction->_source = source;
57
+ previewInteraction->_prepare = [prepare copy];
48
58
  previewInteraction->_commit = [commit copy];
49
59
  previewInteraction->_dismissed = [dismissed copy];
50
60
  previewInteraction->_interaction =
@@ -91,7 +101,9 @@ static NSMutableSet *ANNPreviews(void) {
91
101
  [view removeInteraction:_interaction];
92
102
  // A tap's commit animation may still run: a preview that went, its screen destroyed and its push failed, is not
93
103
  // pushed when the animation ends.
104
+ _prepare = nil;
94
105
  _commit = nil;
106
+ [ANNPreviewInteraction removeSnapshots:_snapshots];
95
107
  void (^dismissed)(void) = _dismissed;
96
108
  _dismissed = nil;
97
109
  if (dismissed) {
@@ -116,6 +128,18 @@ static NSMutableSet *ANNPreviews(void) {
116
128
  [self detach];
117
129
  return nil;
118
130
  }
131
+ // UIKit ends a lift that never became a menu too (the touch ended before the long press); should
132
+ // it not, the snapshot of that press goes now.
133
+ if (_snapshots != _endingSnapshots) {
134
+ [ANNPreviewInteraction removeSnapshots:_snapshots];
135
+ }
136
+ _snapshots = [NSMutableArray new];
137
+ // UIKit asks for the configuration as the press starts, about half a second before it shows the preview: the
138
+ // screen renders now, so that its content is there by then.
139
+ if (_prepare && !_prepare()) {
140
+ [self detach];
141
+ return nil;
142
+ }
119
143
  UIViewController *preview = _preview;
120
144
  NSArray *actions = preview.resolveOptions.preview.actions;
121
145
  return [UIContextMenuConfiguration
@@ -129,6 +153,23 @@ static NSMutableSet *ANNPreviews(void) {
129
153
  }];
130
154
  }
131
155
 
156
+ // The menu goes: the snapshots of its press go once it is gone. UIKit asks for the one
157
+ // it goes back to after this call, so the list is read at the end, not now.
158
+ - (void)contextMenuInteraction:(UIContextMenuInteraction *)interaction
159
+ willEndForConfiguration:(UIContextMenuConfiguration *)configuration
160
+ animator:(id<UIContextMenuInteractionAnimating>)animator {
161
+ NSMutableArray<UIView *> *snapshots = _snapshots;
162
+ _endingSnapshots = snapshots;
163
+ void (^remove)(void) = ^{
164
+ [ANNPreviewInteraction removeSnapshots:snapshots];
165
+ };
166
+ if (animator) {
167
+ [animator addCompletion:remove];
168
+ } else {
169
+ dispatch_async(dispatch_get_main_queue(), remove);
170
+ }
171
+ }
172
+
132
173
  - (void)contextMenuInteraction:(UIContextMenuInteraction *)interaction
133
174
  willPerformPreviewActionForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration
134
175
  animator:(id<UIContextMenuInteractionCommitAnimating>)
@@ -148,24 +189,45 @@ static NSMutableSet *ANNPreviews(void) {
148
189
 
149
190
  #pragma mark - Targeted previews
150
191
 
151
- // The view the menu lifts from and goes back to: a snapshot of the source view in its window. With
152
- // the source view itself UIKit adds its views next to it, among the React views of its parent, and
153
- // Fabric then mounts and unmounts at the wrong places.
192
+ // The view the menu lifts from and goes back to: a snapshot of the source view, laid over it in the
193
+ // view of its screen, the core's own (in its window when the view is elsewhere), so that it goes with
194
+ // the screen (a pop right after the menu). With the source view itself UIKit adds its views next to
195
+ // it, among the React views of its parent, and Fabric then mounts and unmounts at the wrong places.
196
+ // A snapshot out of the window, with only a target there, iOS 27 doesn't lift: it dims it in place.
154
197
  - (UITargetedPreview *)snapshotPreviewOf:(UIView *)view {
155
- UIWindow *window = view.window;
198
+ UIView *container = _source.viewIfLoaded;
199
+ if (!container || ![view isDescendantOfView:container]) {
200
+ container = view.window;
201
+ }
202
+ // A screen out of its window (a command took it away while the menu showed) has no place for it:
203
+ // UIKit needs the view in a window.
204
+ if (!container.window) {
205
+ return nil;
206
+ }
156
207
  UIView *snapshot = [view snapshotViewAfterScreenUpdates:NO];
157
- if (!window || !snapshot) {
208
+ if (!snapshot) {
158
209
  return nil;
159
210
  }
211
+ snapshot.frame = [view convertRect:view.bounds toView:container];
212
+ snapshot.userInteractionEnabled = NO;
213
+ snapshot.accessibilityElementsHidden = YES;
214
+ [container addSubview:snapshot];
215
+ if (!_snapshots) {
216
+ _snapshots = [NSMutableArray new];
217
+ }
218
+ [_snapshots addObject:snapshot];
160
219
  UIPreviewParameters *parameters = [UIPreviewParameters new];
161
220
  parameters.backgroundColor = UIColor.clearColor;
162
221
  parameters.visiblePath = [UIBezierPath bezierPathWithRoundedRect:view.bounds
163
222
  cornerRadius:view.layer.cornerRadius];
164
- CGPoint center = [view convertPoint:CGPointMake(CGRectGetMidX(view.bounds),
165
- CGRectGetMidY(view.bounds))
166
- toView:window];
167
- UIPreviewTarget *target = [[UIPreviewTarget alloc] initWithContainer:window center:center];
168
- return [[UITargetedPreview alloc] initWithView:snapshot parameters:parameters target:target];
223
+ return [[UITargetedPreview alloc] initWithView:snapshot parameters:parameters];
224
+ }
225
+
226
+ + (void)removeSnapshots:(NSMutableArray<UIView *> *)snapshots {
227
+ for (UIView *snapshot in snapshots) {
228
+ [snapshot removeFromSuperview];
229
+ }
230
+ [snapshots removeAllObjects];
169
231
  }
170
232
 
171
233
  - (UITargetedPreview *)contextMenuInteraction:(UIContextMenuInteraction *)interaction