@lodev09/react-native-true-sheet 4.0.0-beta.0 → 4.0.0-beta.10

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 (430) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +62 -30
  3. package/RNTrueSheet.podspec +10 -2
  4. package/android/build.gradle +24 -2
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +189 -47
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerViewManager.kt +7 -0
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +401 -19
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +18 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +145 -21
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +14 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderView.kt +38 -0
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderViewManager.kt +28 -0
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +84 -24
  14. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +106 -0
  15. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayViewManager.kt +45 -0
  16. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +6 -4
  17. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekView.kt +72 -0
  18. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPeekViewManager.kt +33 -0
  19. package/android/src/main/java/com/lodev09/truesheet/TrueSheetStateUpdater.kt +63 -0
  20. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +564 -226
  21. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +1399 -598
  22. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +161 -29
  23. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensEventObserver.kt +63 -0
  24. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetBehavior.kt +36 -0
  25. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +255 -0
  26. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +375 -0
  27. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +299 -0
  28. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +162 -0
  29. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +180 -0
  30. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetKeyboardObserver.kt +207 -0
  31. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetOverlayRootView.kt +137 -0
  32. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetStackManager.kt +220 -0
  33. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetDragEvents.kt +71 -0
  34. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetFocusEvents.kt +65 -0
  35. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +127 -0
  36. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetStateEvents.kt +56 -0
  37. package/android/src/main/java/com/lodev09/truesheet/utils/KeyboardUtils.kt +84 -0
  38. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +93 -67
  39. package/android/src/main/java/com/lodev09/truesheet/utils/TouchEventDeduper.kt +31 -0
  40. package/android/src/main/java/com/lodev09/truesheet/utils/ViewUtils.kt +42 -0
  41. package/android/src/main/jni/CMakeLists.txt +63 -0
  42. package/android/src/main/jni/TrueSheetSpec.h +20 -0
  43. package/android/src/main/jni/TrueSheetStateUpdater.cpp +96 -0
  44. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewComponentDescriptor.h +16 -0
  45. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.cpp +68 -0
  46. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewShadowNode.h +33 -0
  47. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.cpp +11 -0
  48. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetContentViewState.h +39 -0
  49. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewComponentDescriptor.h +24 -0
  50. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.cpp +63 -0
  51. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewShadowNode.h +28 -0
  52. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.cpp +11 -0
  53. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetFooterViewState.h +41 -0
  54. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.cpp +18 -0
  55. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetInsets.h +16 -0
  56. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetLayoutUtils.h +46 -0
  57. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewComponentDescriptor.h +21 -0
  58. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.cpp +17 -0
  59. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetOverlayViewShadowNode.h +36 -0
  60. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h +28 -0
  61. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +30 -0
  62. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h +45 -0
  63. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.cpp +23 -0
  64. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewState.h +52 -0
  65. package/ios/TrueSheetContainerView.h +88 -6
  66. package/ios/TrueSheetContainerView.mm +278 -21
  67. package/ios/TrueSheetContentView.h +54 -1
  68. package/ios/TrueSheetContentView.mm +371 -47
  69. package/ios/TrueSheetFooterView.h +33 -2
  70. package/ios/TrueSheetFooterView.mm +187 -35
  71. package/ios/{events/OnWillDismissEvent.h → TrueSheetHeaderView.h} +10 -7
  72. package/ios/TrueSheetHeaderView.mm +64 -0
  73. package/ios/TrueSheetModule.h +2 -7
  74. package/ios/TrueSheetModule.mm +98 -20
  75. package/ios/TrueSheetOverlayView.h +32 -0
  76. package/ios/TrueSheetOverlayView.mm +161 -0
  77. package/ios/TrueSheetPeekView.h +32 -0
  78. package/ios/TrueSheetPeekView.mm +99 -0
  79. package/ios/TrueSheetView.h +7 -15
  80. package/ios/TrueSheetView.mm +775 -215
  81. package/ios/TrueSheetViewController.h +94 -16
  82. package/ios/TrueSheetViewController.mm +1316 -331
  83. package/ios/core/RNScreensEventObserver.h +46 -0
  84. package/ios/core/RNScreensEventObserver.mm +280 -0
  85. package/ios/core/TrueSheetBlurView.h +27 -0
  86. package/ios/core/TrueSheetBlurView.mm +82 -0
  87. package/ios/core/TrueSheetDetentCalculator.h +107 -0
  88. package/ios/core/TrueSheetDetentCalculator.mm +197 -0
  89. package/ios/core/TrueSheetGrabberView.h +72 -0
  90. package/ios/core/TrueSheetGrabberView.mm +188 -0
  91. package/ios/core/TrueSheetKeyboardObserver.h +39 -0
  92. package/ios/core/TrueSheetKeyboardObserver.mm +112 -0
  93. package/ios/core/TrueSheetOverlayContainerView.h +31 -0
  94. package/ios/core/TrueSheetOverlayContainerView.mm +35 -0
  95. package/ios/events/TrueSheetDragEvents.h +39 -0
  96. package/ios/events/TrueSheetDragEvents.mm +62 -0
  97. package/ios/events/{OnPositionChangeEvent.h → TrueSheetFocusEvents.h} +8 -5
  98. package/ios/events/TrueSheetFocusEvents.mm +49 -0
  99. package/ios/events/TrueSheetLifecycleEvents.h +42 -0
  100. package/ios/events/TrueSheetLifecycleEvents.mm +79 -0
  101. package/ios/events/TrueSheetStateEvents.h +35 -0
  102. package/ios/events/TrueSheetStateEvents.mm +49 -0
  103. package/ios/tvos/TrueSheetStubs.mm +166 -0
  104. package/ios/{events/OnMountEvent.h → utils/BlurUtil.h} +5 -7
  105. package/ios/utils/BlurUtil.mm +69 -0
  106. package/ios/utils/GestureUtil.h +7 -0
  107. package/ios/utils/GestureUtil.mm +12 -0
  108. package/ios/utils/PlatformUtil.h +15 -0
  109. package/ios/utils/UIView+FirstResponder.h +15 -0
  110. package/ios/utils/UIView+FirstResponder.mm +26 -0
  111. package/ios/utils/UIView+ScrollEdgeInteraction.h +26 -0
  112. package/ios/utils/UIView+ScrollEdgeInteraction.mm +84 -0
  113. package/ios/utils/WindowUtil.mm +17 -1
  114. package/lib/module/TrueSheet.js +349 -74
  115. package/lib/module/TrueSheet.js.map +1 -1
  116. package/lib/module/TrueSheet.web.js +1270 -0
  117. package/lib/module/TrueSheet.web.js.map +1 -0
  118. package/lib/module/TrueSheetOverlay.js +30 -0
  119. package/lib/module/TrueSheetOverlay.js.map +1 -0
  120. package/lib/module/TrueSheetOverlay.web.js +35 -0
  121. package/lib/module/TrueSheetOverlay.web.js.map +1 -0
  122. package/lib/module/TrueSheetPeek.js +15 -0
  123. package/lib/module/TrueSheetPeek.js.map +1 -0
  124. package/lib/module/TrueSheetPeek.web.js +55 -0
  125. package/lib/module/TrueSheetPeek.web.js.map +1 -0
  126. package/lib/module/TrueSheetProvider.js +28 -0
  127. package/lib/module/TrueSheetProvider.js.map +1 -0
  128. package/lib/module/TrueSheetProvider.web.js +157 -0
  129. package/lib/module/TrueSheetProvider.web.js.map +1 -0
  130. package/lib/module/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
  131. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
  132. package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
  133. package/lib/module/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
  134. package/lib/module/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  135. package/lib/module/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
  136. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +110 -18
  137. package/lib/module/index.js +4 -3
  138. package/lib/module/index.js.map +1 -1
  139. package/lib/module/mocks/index.js +103 -0
  140. package/lib/module/mocks/index.js.map +1 -0
  141. package/lib/module/mocks/navigation.js +95 -0
  142. package/lib/module/mocks/navigation.js.map +1 -0
  143. package/lib/module/mocks/navigationExpoRouter.js +21 -0
  144. package/lib/module/mocks/navigationExpoRouter.js.map +1 -0
  145. package/lib/module/mocks/reanimated.js +91 -0
  146. package/lib/module/mocks/reanimated.js.map +1 -0
  147. package/lib/module/navigation/TrueSheetNavigatorContent.js +73 -0
  148. package/lib/module/navigation/TrueSheetNavigatorContent.js.map +1 -0
  149. package/lib/module/navigation/actions.js +50 -0
  150. package/lib/module/navigation/actions.js.map +1 -0
  151. package/lib/module/navigation/base-types.js +2 -0
  152. package/lib/module/navigation/base-types.js.map +1 -0
  153. package/lib/module/navigation/createTrueSheetRouter.js +157 -0
  154. package/lib/module/navigation/createTrueSheetRouter.js.map +1 -0
  155. package/lib/module/navigation/expo-router/base-types.js +2 -0
  156. package/lib/module/navigation/expo-router/base-types.js.map +1 -0
  157. package/lib/module/navigation/expo-router/index.js +50 -0
  158. package/lib/module/navigation/expo-router/index.js.map +1 -0
  159. package/lib/module/navigation/expo-router/types.js +4 -0
  160. package/lib/module/navigation/expo-router/types.js.map +1 -0
  161. package/lib/module/navigation/index.js +47 -0
  162. package/lib/module/navigation/index.js.map +1 -0
  163. package/lib/module/navigation/navigator.js +10 -0
  164. package/lib/module/navigation/navigator.js.map +1 -0
  165. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +56 -0
  166. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -0
  167. package/lib/module/navigation/screen/TrueSheetScreen.js +49 -0
  168. package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -0
  169. package/lib/module/navigation/screen/index.js +5 -0
  170. package/lib/module/navigation/screen/index.js.map +1 -0
  171. package/lib/module/navigation/screen/types.js +4 -0
  172. package/lib/module/navigation/screen/types.js.map +1 -0
  173. package/lib/module/navigation/screen/useSheetScreenState.js +69 -0
  174. package/lib/module/navigation/screen/useSheetScreenState.js.map +1 -0
  175. package/lib/module/navigation/types.js +4 -0
  176. package/lib/module/navigation/types.js.map +1 -0
  177. package/lib/module/navigation/useTrueSheetNavigation.js +24 -0
  178. package/lib/module/navigation/useTrueSheetNavigation.js.map +1 -0
  179. package/lib/module/reanimated/ReanimatedTrueSheet.js +15 -9
  180. package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -1
  181. package/lib/module/reanimated/ReanimatedTrueSheet.web.js +81 -0
  182. package/lib/module/reanimated/ReanimatedTrueSheet.web.js.map +1 -0
  183. package/lib/module/reanimated/ReanimatedTrueSheetProvider.js +4 -2
  184. package/lib/module/reanimated/ReanimatedTrueSheetProvider.js.map +1 -1
  185. package/lib/module/reanimated/index.js +2 -2
  186. package/lib/module/reanimated/index.js.map +1 -1
  187. package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js +20 -0
  188. package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js.map +1 -0
  189. package/lib/module/specs/NativeTrueSheetModule.js.map +1 -1
  190. package/lib/module/web/constants.js +16 -0
  191. package/lib/module/web/constants.js.map +1 -0
  192. package/lib/module/web/dom.js +10 -0
  193. package/lib/module/web/dom.js.map +1 -0
  194. package/lib/module/web/vaul/README.md +9 -0
  195. package/lib/module/web/vaul/browser.js +30 -0
  196. package/lib/module/web/vaul/browser.js.map +1 -0
  197. package/lib/module/web/vaul/constants.js +19 -0
  198. package/lib/module/web/vaul/constants.js.map +1 -0
  199. package/lib/module/web/vaul/context.js +59 -0
  200. package/lib/module/web/vaul/context.js.map +1 -0
  201. package/lib/module/web/vaul/helpers.js +99 -0
  202. package/lib/module/web/vaul/helpers.js.map +1 -0
  203. package/lib/module/web/vaul/index.js +1342 -0
  204. package/lib/module/web/vaul/index.js.map +1 -0
  205. package/lib/module/web/vaul/style.css +287 -0
  206. package/lib/module/web/vaul/style.css.d.js +2 -0
  207. package/lib/module/web/vaul/style.css.d.js.map +1 -0
  208. package/lib/module/web/vaul/types.js +2 -0
  209. package/lib/module/web/vaul/types.js.map +1 -0
  210. package/lib/module/web/vaul/use-composed-refs.js +34 -0
  211. package/lib/module/web/vaul/use-composed-refs.js.map +1 -0
  212. package/lib/module/web/vaul/use-controllable-state.js +54 -0
  213. package/lib/module/web/vaul/use-controllable-state.js.map +1 -0
  214. package/lib/module/web/vaul/use-position-fixed.js +123 -0
  215. package/lib/module/web/vaul/use-position-fixed.js.map +1 -0
  216. package/lib/module/web/vaul/use-prevent-scroll.js +258 -0
  217. package/lib/module/web/vaul/use-prevent-scroll.js.map +1 -0
  218. package/lib/module/web/vaul/use-scale-background.js +57 -0
  219. package/lib/module/web/vaul/use-scale-background.js.map +1 -0
  220. package/lib/module/web/vaul/use-snap-points.js +305 -0
  221. package/lib/module/web/vaul/use-snap-points.js.map +1 -0
  222. package/lib/typescript/src/TrueSheet.d.ts +57 -14
  223. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  224. package/lib/typescript/src/TrueSheet.types.d.ts +535 -55
  225. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  226. package/lib/typescript/src/TrueSheet.web.d.ts +5 -0
  227. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -0
  228. package/lib/typescript/src/TrueSheetOverlay.d.ts +8 -0
  229. package/lib/typescript/src/TrueSheetOverlay.d.ts.map +1 -0
  230. package/lib/typescript/src/TrueSheetOverlay.web.d.ts +8 -0
  231. package/lib/typescript/src/TrueSheetOverlay.web.d.ts.map +1 -0
  232. package/lib/typescript/src/TrueSheetPeek.d.ts +9 -0
  233. package/lib/typescript/src/TrueSheetPeek.d.ts.map +1 -0
  234. package/lib/typescript/src/TrueSheetPeek.web.d.ts +28 -0
  235. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -0
  236. package/lib/typescript/src/TrueSheetProvider.d.ts +18 -0
  237. package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -0
  238. package/lib/typescript/src/TrueSheetProvider.web.d.ts +27 -0
  239. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -0
  240. package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts.map +1 -1
  241. package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts.map +1 -1
  242. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +2 -0
  243. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -1
  244. package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts +6 -0
  245. package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts.map +1 -0
  246. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts +6 -0
  247. package/lib/typescript/src/fabric/TrueSheetOverlayViewNativeComponent.d.ts.map +1 -0
  248. package/lib/typescript/src/fabric/TrueSheetPeekViewNativeComponent.d.ts +6 -0
  249. package/lib/typescript/src/fabric/TrueSheetPeekViewNativeComponent.d.ts.map +1 -0
  250. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +71 -16
  251. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  252. package/lib/typescript/src/index.d.ts +3 -2
  253. package/lib/typescript/src/index.d.ts.map +1 -1
  254. package/lib/typescript/src/mocks/index.d.ts +47 -0
  255. package/lib/typescript/src/mocks/index.d.ts.map +1 -0
  256. package/lib/typescript/src/mocks/navigation.d.ts +70 -0
  257. package/lib/typescript/src/mocks/navigation.d.ts.map +1 -0
  258. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts +22 -0
  259. package/lib/typescript/src/mocks/navigationExpoRouter.d.ts.map +1 -0
  260. package/lib/typescript/src/mocks/reanimated.d.ts +48 -0
  261. package/lib/typescript/src/mocks/reanimated.d.ts.map +1 -0
  262. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts +5 -0
  263. package/lib/typescript/src/navigation/TrueSheetNavigatorContent.d.ts.map +1 -0
  264. package/lib/typescript/src/navigation/actions.d.ts +71 -0
  265. package/lib/typescript/src/navigation/actions.d.ts.map +1 -0
  266. package/lib/typescript/src/navigation/base-types.d.ts +9 -0
  267. package/lib/typescript/src/navigation/base-types.d.ts.map +1 -0
  268. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts +3 -0
  269. package/lib/typescript/src/navigation/createTrueSheetRouter.d.ts.map +1 -0
  270. package/lib/typescript/src/navigation/expo-router/base-types.d.ts +8 -0
  271. package/lib/typescript/src/navigation/expo-router/base-types.d.ts.map +1 -0
  272. package/lib/typescript/src/navigation/expo-router/index.d.ts +37 -0
  273. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -0
  274. package/lib/typescript/src/navigation/expo-router/types.d.ts +44 -0
  275. package/lib/typescript/src/navigation/expo-router/types.d.ts.map +1 -0
  276. package/lib/typescript/src/navigation/index.d.ts +45 -0
  277. package/lib/typescript/src/navigation/index.d.ts.map +1 -0
  278. package/lib/typescript/src/navigation/navigator.d.ts +7 -0
  279. package/lib/typescript/src/navigation/navigator.d.ts.map +1 -0
  280. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts +3 -0
  281. package/lib/typescript/src/navigation/screen/ReanimatedTrueSheetScreen.d.ts.map +1 -0
  282. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts +3 -0
  283. package/lib/typescript/src/navigation/screen/TrueSheetScreen.d.ts.map +1 -0
  284. package/lib/typescript/src/navigation/screen/index.d.ts +4 -0
  285. package/lib/typescript/src/navigation/screen/index.d.ts.map +1 -0
  286. package/lib/typescript/src/navigation/screen/types.d.ts +14 -0
  287. package/lib/typescript/src/navigation/screen/types.d.ts.map +1 -0
  288. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts +34 -0
  289. package/lib/typescript/src/navigation/screen/useSheetScreenState.d.ts.map +1 -0
  290. package/lib/typescript/src/navigation/types.d.ts +213 -0
  291. package/lib/typescript/src/navigation/types.d.ts.map +1 -0
  292. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts +23 -0
  293. package/lib/typescript/src/navigation/useTrueSheetNavigation.d.ts.map +1 -0
  294. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts.map +1 -1
  295. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts +41 -0
  296. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts.map +1 -0
  297. package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts +8 -2
  298. package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts.map +1 -1
  299. package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts +15 -0
  300. package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts.map +1 -0
  301. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +25 -3
  302. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -1
  303. package/lib/typescript/src/web/constants.d.ts +14 -0
  304. package/lib/typescript/src/web/constants.d.ts.map +1 -0
  305. package/lib/typescript/src/web/dom.d.ts +7 -0
  306. package/lib/typescript/src/web/dom.d.ts.map +1 -0
  307. package/lib/typescript/src/web/vaul/browser.d.ts +8 -0
  308. package/lib/typescript/src/web/vaul/browser.d.ts.map +1 -0
  309. package/lib/typescript/src/web/vaul/constants.d.ts +14 -0
  310. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -0
  311. package/lib/typescript/src/web/vaul/context.d.ts +54 -0
  312. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -0
  313. package/lib/typescript/src/web/vaul/helpers.d.ts +17 -0
  314. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -0
  315. package/lib/typescript/src/web/vaul/index.d.ts +234 -0
  316. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -0
  317. package/lib/typescript/src/web/vaul/types.d.ts +7 -0
  318. package/lib/typescript/src/web/vaul/types.d.ts.map +1 -0
  319. package/lib/typescript/src/web/vaul/use-composed-refs.d.ts +14 -0
  320. package/lib/typescript/src/web/vaul/use-composed-refs.d.ts.map +1 -0
  321. package/lib/typescript/src/web/vaul/use-controllable-state.d.ts +9 -0
  322. package/lib/typescript/src/web/vaul/use-controllable-state.d.ts.map +1 -0
  323. package/lib/typescript/src/web/vaul/use-position-fixed.d.ts +19 -0
  324. package/lib/typescript/src/web/vaul/use-position-fixed.d.ts.map +1 -0
  325. package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts +18 -0
  326. package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts.map +1 -0
  327. package/lib/typescript/src/web/vaul/use-scale-background.d.ts +2 -0
  328. package/lib/typescript/src/web/vaul/use-scale-background.d.ts.map +1 -0
  329. package/lib/typescript/src/web/vaul/use-snap-points.d.ts +38 -0
  330. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -0
  331. package/package.json +104 -37
  332. package/react-native.config.js +10 -2
  333. package/src/TrueSheet.tsx +424 -70
  334. package/src/TrueSheet.types.ts +602 -72
  335. package/src/TrueSheet.web.tsx +1437 -0
  336. package/src/TrueSheetOverlay.tsx +24 -0
  337. package/src/TrueSheetOverlay.web.tsx +33 -0
  338. package/src/TrueSheetPeek.tsx +11 -0
  339. package/src/TrueSheetPeek.web.tsx +64 -0
  340. package/src/TrueSheetProvider.tsx +33 -0
  341. package/src/TrueSheetProvider.web.tsx +220 -0
  342. package/src/fabric/TrueSheetContainerViewNativeComponent.ts +1 -1
  343. package/src/fabric/TrueSheetContentViewNativeComponent.ts +5 -1
  344. package/src/fabric/TrueSheetFooterViewNativeComponent.ts +10 -2
  345. package/src/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
  346. package/src/fabric/TrueSheetOverlayViewNativeComponent.ts +12 -0
  347. package/src/fabric/TrueSheetPeekViewNativeComponent.ts +8 -0
  348. package/src/fabric/TrueSheetViewNativeComponent.ts +110 -18
  349. package/src/index.ts +3 -2
  350. package/src/mocks/index.ts +96 -0
  351. package/src/mocks/navigation.ts +79 -0
  352. package/src/mocks/navigationExpoRouter.ts +41 -0
  353. package/src/mocks/reanimated.ts +100 -0
  354. package/src/navigation/TrueSheetNavigatorContent.tsx +106 -0
  355. package/src/navigation/actions.ts +78 -0
  356. package/src/navigation/base-types.ts +20 -0
  357. package/src/navigation/createTrueSheetRouter.ts +194 -0
  358. package/src/navigation/expo-router/base-types.ts +18 -0
  359. package/src/navigation/expo-router/index.ts +90 -0
  360. package/src/navigation/expo-router/types.ts +116 -0
  361. package/src/navigation/index.ts +80 -0
  362. package/src/navigation/navigator.ts +18 -0
  363. package/src/navigation/screen/ReanimatedTrueSheetScreen.tsx +61 -0
  364. package/src/navigation/screen/TrueSheetScreen.tsx +54 -0
  365. package/src/navigation/screen/index.ts +3 -0
  366. package/src/navigation/screen/types.ts +19 -0
  367. package/src/navigation/screen/useSheetScreenState.ts +102 -0
  368. package/src/navigation/types.ts +302 -0
  369. package/src/navigation/useTrueSheetNavigation.ts +26 -0
  370. package/src/reanimated/ReanimatedTrueSheet.tsx +12 -7
  371. package/src/reanimated/ReanimatedTrueSheet.web.tsx +75 -0
  372. package/src/reanimated/ReanimatedTrueSheetProvider.tsx +12 -8
  373. package/src/reanimated/useReanimatedPositionChangeHandler.web.ts +31 -0
  374. package/src/specs/NativeTrueSheetModule.ts +28 -3
  375. package/src/web/constants.ts +13 -0
  376. package/src/web/dom.ts +9 -0
  377. package/src/web/vaul/README.md +9 -0
  378. package/src/web/vaul/browser.ts +38 -0
  379. package/src/web/vaul/constants.ts +24 -0
  380. package/src/web/vaul/context.ts +102 -0
  381. package/src/web/vaul/helpers.ts +131 -0
  382. package/src/web/vaul/index.tsx +1785 -0
  383. package/src/web/vaul/style.css +287 -0
  384. package/src/web/vaul/style.css.d.ts +1 -0
  385. package/src/web/vaul/types.ts +7 -0
  386. package/src/web/vaul/use-composed-refs.ts +35 -0
  387. package/src/web/vaul/use-controllable-state.ts +66 -0
  388. package/src/web/vaul/use-position-fixed.ts +147 -0
  389. package/src/web/vaul/use-prevent-scroll.ts +309 -0
  390. package/src/web/vaul/use-scale-background.ts +64 -0
  391. package/src/web/vaul/use-snap-points.ts +455 -0
  392. package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +0 -26
  393. package/android/src/main/java/com/lodev09/truesheet/events/DidDismissEvent.kt +0 -20
  394. package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +0 -26
  395. package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +0 -26
  396. package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +0 -26
  397. package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +0 -26
  398. package/android/src/main/java/com/lodev09/truesheet/events/MountEvent.kt +0 -20
  399. package/android/src/main/java/com/lodev09/truesheet/events/PositionChangeEvent.kt +0 -32
  400. package/android/src/main/java/com/lodev09/truesheet/events/SizeChangeEvent.kt +0 -27
  401. package/android/src/main/java/com/lodev09/truesheet/events/WillDismissEvent.kt +0 -20
  402. package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +0 -26
  403. package/android/src/main/res/values/styles.xml +0 -8
  404. package/ios/events/OnDetentChangeEvent.h +0 -28
  405. package/ios/events/OnDetentChangeEvent.mm +0 -30
  406. package/ios/events/OnDidDismissEvent.h +0 -26
  407. package/ios/events/OnDidDismissEvent.mm +0 -25
  408. package/ios/events/OnDidPresentEvent.h +0 -28
  409. package/ios/events/OnDidPresentEvent.mm +0 -30
  410. package/ios/events/OnDragBeginEvent.h +0 -28
  411. package/ios/events/OnDragBeginEvent.mm +0 -30
  412. package/ios/events/OnDragChangeEvent.h +0 -28
  413. package/ios/events/OnDragChangeEvent.mm +0 -30
  414. package/ios/events/OnDragEndEvent.h +0 -28
  415. package/ios/events/OnDragEndEvent.mm +0 -30
  416. package/ios/events/OnMountEvent.mm +0 -25
  417. package/ios/events/OnPositionChangeEvent.mm +0 -32
  418. package/ios/events/OnSizeChangeEvent.h +0 -28
  419. package/ios/events/OnSizeChangeEvent.mm +0 -30
  420. package/ios/events/OnWillDismissEvent.mm +0 -25
  421. package/ios/events/OnWillPresentEvent.h +0 -28
  422. package/ios/events/OnWillPresentEvent.mm +0 -30
  423. package/ios/utils/LayoutUtil.h +0 -44
  424. package/ios/utils/LayoutUtil.mm +0 -50
  425. package/lib/module/TrueSheetGrabber.js +0 -51
  426. package/lib/module/TrueSheetGrabber.js.map +0 -1
  427. package/lib/typescript/src/TrueSheetGrabber.d.ts +0 -39
  428. package/lib/typescript/src/TrueSheetGrabber.d.ts.map +0 -1
  429. package/src/TrueSheetGrabber.tsx +0 -82
  430. package/src/__mocks__/index.js +0 -67
@@ -7,200 +7,809 @@
7
7
  //
8
8
 
9
9
  #import "TrueSheetViewController.h"
10
+ #import "TrueSheetContainerView.h"
11
+ #import "TrueSheetContentView.h"
12
+ #import "core/TrueSheetBlurView.h"
13
+ #import "core/TrueSheetDetentCalculator.h"
14
+ #import "core/TrueSheetGrabberView.h"
15
+ #import "utils/BlurUtil.h"
16
+ #import "utils/GestureUtil.h"
17
+ #import "utils/PlatformUtil.h"
10
18
  #import "utils/WindowUtil.h"
11
19
 
12
20
  #import <React/RCTLog.h>
13
21
  #import <React/RCTScrollViewComponentView.h>
14
- #import "TrueSheetContentView.h"
15
- #import "utils/GestureUtil.h"
22
+ #import <objc/runtime.h>
23
+ #import <react/renderer/components/TrueSheetSpec/Props.h>
24
+
25
+ using namespace facebook::react;
26
+
27
+ typedef struct {
28
+ CGFloat position;
29
+ CGFloat detent;
30
+ CGFloat index;
31
+ } TrueSheetPositionState;
32
+
33
+ static BOOL TrueSheetPositionStateEquals(TrueSheetPositionState a, TrueSheetPositionState b) {
34
+ return fabs(a.position - b.position) <= 0.01 && fabs(a.detent - b.detent) <= 0.01 && fabs(a.index - b.index) <= 0.01;
35
+ }
36
+
37
+ static char TrueSheetAccessibilityWindowOwnerKey;
38
+ static char TrueSheetAccessibilityWindowPreviousElementsKey;
16
39
 
17
40
  @interface TrueSheetViewController ()
18
41
 
42
+ - (UIViewController *)accessibilityPresentingViewController;
43
+ - (void)setupTransitionTracker;
44
+ - (void)settleAtDetentIndex:(NSInteger)index debug:(NSString *)debug;
45
+ - (void)restoreWindowAccessibilityElements;
46
+ - (void)setSheetAccessibilityElementsHidden:(BOOL)hidden;
47
+ - (void)setAccessibilityContentElement:(UIView *)contentView;
48
+ - (NSArray *)accessibilityGrabberElements;
49
+ - (void)endInteractiveDismissState;
50
+ - (void)animateInteractiveContainerToTransform:(CGAffineTransform)transform
51
+ duration:(NSTimeInterval)duration
52
+ allowUserInteraction:(BOOL)allowUserInteraction
53
+ completion:(void (^)(void))completion;
54
+
55
+ @end
56
+
57
+ // VoiceOver delivers the two-finger Z (escape) gesture by walking up the view
58
+ // hierarchy from the focused element, so the sheet's root view handles it.
59
+ @interface TrueSheetControllerView : UIView
60
+ @property (nonatomic, weak) TrueSheetViewController *controller;
61
+ @end
62
+
63
+ @implementation TrueSheetControllerView
64
+
65
+ - (BOOL)accessibilityPerformEscape {
66
+ return [self.controller accessibilityPerformEscape];
67
+ }
68
+
19
69
  @end
20
70
 
21
71
  @implementation TrueSheetViewController {
22
- CGFloat _lastPosition;
23
- CGFloat _lastTransitionPosition;
24
- UIVisualEffectView *_backgroundView;
25
- CGFloat _bottomInset;
72
+ TrueSheetPositionState _lastEmittedPositionState;
73
+ CGSize _lastReportedSize;
74
+ CGFloat _autoDetentHeight;
75
+ CGFloat _peekDetentHeight;
76
+ NSInteger _pendingDetentIndex;
77
+
78
+ // Present/dismiss tracking: a display link scoped to the transition
79
+ // coordinator — started alongside the transition, stopped in its completion.
80
+ CADisplayLink *_transitionLink;
26
81
  BOOL _isTransitioning;
82
+
83
+ BOOL _pendingContentSizeChange;
84
+ BOOL _pendingDetentsChange;
27
85
  BOOL _isDragging;
28
- BOOL _isTrackingPositionFromLayout;
29
- UIView *_fakeTransitionView;
30
- CADisplayLink *_displayLink;
86
+ BOOL _isWillDismissEmitted;
87
+
88
+ // Last bottom safe area observed while presented — reused as the estimate
89
+ // for the next present (beats the float-threshold guess for re-presents).
90
+ BOOL _hasObservedBottomInset;
91
+ CGFloat _observedBottomInset;
92
+
93
+ BOOL _isInteractiveDismiss;
94
+ CGFloat _interactiveStartPosition;
95
+ UIView *_interactiveContainerView;
96
+ NSUInteger _interactiveGeneration;
97
+ CADisplayLink *_interactivePositionLink;
98
+
99
+ __weak TrueSheetViewController *_parentSheetController;
100
+
101
+ UIView *_anchorView;
102
+
103
+ __weak UIWindow *_accessibilityWindow;
104
+
105
+ TrueSheetBlurView *_blurView;
106
+ TrueSheetGrabberView *_grabberView;
107
+ TrueSheetDetentCalculator *_detentCalculator;
31
108
  }
32
109
 
110
+ #pragma mark - Initialization
111
+
33
112
  - (instancetype)init {
34
113
  if (self = [super initWithNibName:nil bundle:nil]) {
35
114
  _detents = @[ @0.5, @1 ];
36
115
  _contentHeight = @(0);
116
+ _headerHeight = @(0);
117
+ _footerHeight = @(0);
118
+ _peekContentHeight = @(0);
37
119
  _grabber = YES;
120
+ _draggable = YES;
121
+ _scrollingExpandsSheet = YES;
122
+ _dismissible = YES;
38
123
  _dimmed = YES;
39
- _dimmedIndex = @(0);
40
- _lastPosition = 0;
41
- _lastTransitionPosition = 0;
42
- _isTransitioning = NO;
124
+ _dimmedDetentIndex = @(0);
125
+ _presentation = facebook::react::TrueSheetViewPresentation::Page;
126
+ _lastEmittedPositionState = (TrueSheetPositionState){0, 0, 0};
43
127
  _isDragging = NO;
44
- _isTrackingPositionFromLayout = NO;
45
- _layoutTransitioning = NO;
46
128
  _isPresented = NO;
129
+ _isWillDismissEmitted = NO;
130
+ _isTransitioning = NO;
131
+ _pendingContentSizeChange = NO;
132
+ _pendingDetentsChange = NO;
47
133
  _activeDetentIndex = -1;
134
+ _pendingDetentIndex = -1;
48
135
 
49
- _backgroundView = [[UIVisualEffectView alloc] init];
50
- _backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
51
-
52
- // Initialize fake transition view for tracking position during animations
53
- _fakeTransitionView = [[UIView alloc] init];
54
- _fakeTransitionView.hidden = YES;
55
- _fakeTransitionView.userInteractionEnabled = NO;
56
-
57
- // Get bottom safe area inset from the window's safe area
58
- // The sheet's view has smaller insets, so we need the actual device insets
59
- UIWindow *window = [WindowUtil keyWindow];
60
- _bottomInset = window ? window.safeAreaInsets.bottom : 0;
136
+ _blurInteraction = YES;
137
+ _insetAdjustment = TrueSheetViewInsetAdjustment::Automatic;
138
+ _detentCalculator = [[TrueSheetDetentCalculator alloc] init];
139
+ _detentCalculator.delegate = self;
61
140
  }
62
141
  return self;
63
142
  }
64
143
 
65
144
  - (void)dealloc {
145
+ [self restoreWindowAccessibilityElements];
146
+ [_transitionLink invalidate];
147
+ _transitionLink = nil;
148
+ [_interactivePositionLink invalidate];
149
+ _interactivePositionLink = nil;
66
150
  [[NSNotificationCenter defaultCenter] removeObserver:self];
67
151
  }
68
152
 
69
- #pragma mark - Presentation State
153
+ #pragma mark - Computed Properties
154
+
155
+ - (UISheetPresentationController *)sheet {
156
+ return self.sheetPresentationController;
157
+ }
70
158
 
71
159
  - (BOOL)isTopmostPresentedController {
72
- // Check if we're in the window hierarchy and visible
73
160
  if (!self.isViewLoaded || self.view.window == nil) {
74
161
  return NO;
75
162
  }
76
-
77
- // Check if another controller is presented on top of this sheet
78
163
  return self.presentedViewController == nil;
79
164
  }
80
165
 
81
- - (BOOL)isActiveAndVisible {
82
- return self.isViewLoaded && self.view.window != nil && !self.isBeingDismissed;
166
+ // YES while a child controller is stacked on top (e.g. a nested sheet)
167
+ // UIKit's push-back scaling mutates the frame while the sheet is backgrounded.
168
+ - (BOOL)isStackedBehindChild {
169
+ UIViewController *presented = self.presentedViewController;
170
+ return presented != nil && !presented.isBeingDismissed;
83
171
  }
84
172
 
85
173
  - (UIView *)presentedView {
86
- return self.sheetPresentationController.presentedView;
174
+ return self.sheet.presentedView;
175
+ }
176
+
177
+ - (CGFloat)currentPosition {
178
+ UIView *presentedView = self.presentedView;
179
+ return presentedView ? presentedView.frame.origin.y : 0.0;
180
+ }
181
+
182
+ // YES when any layer from the presented view up to the window has in-flight
183
+ // animations (detent snap, present/dismiss transition, container settle).
184
+ - (BOOL)isPresentedViewAnimating {
185
+ for (CALayer *layer = self.presentedView.layer; layer; layer = layer.superlayer) {
186
+ if (layer.animationKeys.count > 0) {
187
+ return YES;
188
+ }
189
+ }
190
+ return NO;
191
+ }
192
+
193
+ /**
194
+ * The sheet's live on-screen position. At rest and during drags (direct frame
195
+ * sets) this is the model position; during animations it adds the
196
+ * presentation-vs-model delta measured in window space, so ancestor animations
197
+ * compose and persistent transforms (e.g. floating-sheet inset) cancel out.
198
+ */
199
+ - (CGFloat)livePosition {
200
+ UIView *presentedView = self.presentedView;
201
+ UIWindow *window = presentedView.window;
202
+ UIView *containerView = self.sheet.containerView;
203
+
204
+ // Direct (non-animated) translation applied during an interactive nav dismiss.
205
+ CGFloat containerTy = containerView ? containerView.transform.ty : 0;
206
+
207
+ if (!presentedView || !window || !self.isPresentedViewAnimating) {
208
+ return self.currentPosition + containerTy;
209
+ }
210
+
211
+ CALayer *modelLayer = presentedView.layer;
212
+ CALayer *presentationLayer = modelLayer.presentationLayer;
213
+ CALayer *rootModel = window.layer;
214
+ CALayer *rootPresentation = rootModel.presentationLayer;
215
+ if (!presentationLayer || !rootPresentation) {
216
+ return self.currentPosition + containerTy;
217
+ }
218
+
219
+ CGFloat modelY = [modelLayer convertPoint:CGPointZero toLayer:rootModel].y;
220
+ CGFloat presentationY = [presentationLayer convertPoint:CGPointZero toLayer:rootPresentation].y;
221
+ return self.currentPosition + (presentationY - modelY) + containerTy;
222
+ }
223
+
224
+ - (CGFloat)screenHeight {
225
+ // In a windowed iPad app (Stage Manager, iPadOS 26 windowing) the window can be shorter than
226
+ // UIScreen.mainScreen, so prefer the window's height. Some callers run before this controller's
227
+ // view is attached (e.g. mid-presentation detent resolution), so fall back to the presenter's.
228
+ UIWindow *window = self.view.window ?: self.presentingViewController.view.window;
229
+ return window ? window.bounds.size.height : UIScreen.mainScreen.bounds.size.height;
230
+ }
231
+
232
+ // Only phone sheets absorb the bottom safe-area inset — non-phone idioms
233
+ // (iPad form sheets) are inset from the screen edge by UIKit.
234
+ static BOOL TrueSheetIsPhoneIdiom(void) {
235
+ return UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone;
236
+ }
237
+
238
+ - (CGFloat)detentBottomAdjustmentForHeight:(CGFloat)height {
239
+ if (_insetAdjustment == TrueSheetViewInsetAdjustment::Automatic) {
240
+ return 0;
241
+ }
242
+
243
+ return [self bottomSafeAreaForHeight:height];
244
+ }
245
+
246
+ - (CGFloat)bottomSafeAreaForHeight:(CGFloat)height {
247
+ if (!TrueSheetIsPhoneIdiom()) {
248
+ return 0;
249
+ }
250
+
251
+ // On iOS 26+, returns 0 for small detents (height <= 150)
252
+ // Floating sheets don't need adjustment
253
+ if (@available(iOS 26.0, *)) {
254
+ if (height <= 150) {
255
+ return 0;
256
+ }
257
+ }
258
+
259
+ UIWindow *window = [WindowUtil keyWindow];
260
+ return window ? window.safeAreaInsets.bottom : 0;
261
+ }
262
+
263
+ /**
264
+ * Largest height the sheet can reach across its configured detents, excluding
265
+ * the inset currently baked into the footer's height. Basis for the footer's
266
+ * inset absorption so it follows the same rule as bottomSafeAreaForHeight: —
267
+ * a sheet that only ever floats (small detents on iOS 26) keeps its footer
268
+ * unpadded.
269
+ */
270
+ - (CGFloat)maxNaturalDetentHeight {
271
+ CGFloat applied = [self.footerHeight floatValue] > 0 ? _appliedFooterBottomInset : 0;
272
+ CGFloat maxHeight = 0;
273
+
274
+ for (id detent in self.detents) {
275
+ if (![detent isKindOfClass:[NSNumber class]]) {
276
+ continue;
277
+ }
278
+
279
+ CGFloat value = [detent doubleValue];
280
+ CGFloat height = 0;
281
+ if (value == -1) {
282
+ // Auto counts a relative footer's (padded) height
283
+ height = [_detentCalculator autoHeight] - (_absoluteFooter ? 0 : applied);
284
+ } else if (value == -2) {
285
+ // Peek counts an absolute footer's (padded) height
286
+ height = [_detentCalculator peekHeight] - (_absoluteFooter ? applied : 0);
287
+ } else if (value > 0 && value <= 1) {
288
+ height = value * self.screenHeight;
289
+ }
290
+ maxHeight = fmax(maxHeight, height);
291
+ }
292
+
293
+ if (self.maxContentHeight) {
294
+ maxHeight = fmin(maxHeight, [self.maxContentHeight floatValue]);
295
+ }
296
+
297
+ return maxHeight > 0 ? maxHeight : self.screenHeight;
298
+ }
299
+
300
+ // The inset the footer absorbs as padding.
301
+ - (CGFloat)footerBottomInset {
302
+ if (_insetAdjustment != TrueSheetViewInsetAdjustment::Automatic) {
303
+ return 0;
304
+ }
305
+
306
+ // Early out so the maxNaturalDetentHeight fallback below doesn't run per
307
+ // layout pass on iPad only to be zeroed by bottomSafeAreaForHeight:.
308
+ if (!TrueSheetIsPhoneIdiom()) {
309
+ return 0;
310
+ }
311
+
312
+ // Once laid out, the sheet's own safe area is the authority — it reflects
313
+ // UIKit's actual float/anchored decision (a floating sheet gets 0), where
314
+ // bottomSafeAreaForHeight: can only guess the float threshold. Guessing
315
+ // wrong leaves the safe-area region unfilled (gap) or double-padded.
316
+ UIView *view = self.viewIfLoaded;
317
+ if (view.window) {
318
+ _hasObservedBottomInset = YES;
319
+ _observedBottomInset = view.safeAreaInsets.bottom;
320
+ return _observedBottomInset;
321
+ }
322
+
323
+ // Off-window while presented (e.g. hidden behind a native screen): the last
324
+ // observed value is this sheet's own truth — prefer it over the
325
+ // float-threshold guess. Cleared on dismiss so a re-present observes fresh
326
+ // geometry (rotation or detent changes while dismissed would stale it).
327
+ if (_hasObservedBottomInset) {
328
+ return _observedBottomInset;
329
+ }
330
+
331
+ return [self bottomSafeAreaForHeight:[self maxNaturalDetentHeight]];
332
+ }
333
+
334
+ /**
335
+ * Bottom inset excluded from the auto detent. A relative footer owns the
336
+ * sheet's bottom edge and carries the inset in its height (see
337
+ * footerBottomInset) — subtract exactly what was baked in so UIKit's own
338
+ * safe-area layout doesn't double it.
339
+ */
340
+ - (CGFloat)autoDetentBottomInset {
341
+ if (_absoluteFooter || [self.footerHeight floatValue] <= 0) {
342
+ return 0;
343
+ }
344
+
345
+ return _appliedFooterBottomInset;
346
+ }
347
+
348
+ /**
349
+ * Bottom inset excluded from the peek detent. An absolute footer counts
350
+ * toward the peek height and carries the inset in its height — subtract
351
+ * exactly what was baked in so it isn't doubled.
352
+ */
353
+ - (CGFloat)peekDetentBottomInset {
354
+ if (!_absoluteFooter || [self.footerHeight floatValue] <= 0) {
355
+ return 0;
356
+ }
357
+
358
+ return _appliedFooterBottomInset;
359
+ }
360
+
361
+ - (BOOL)isDesignCompatibilityMode {
362
+ if (@available(iOS 26.0, *)) {
363
+ NSNumber *value = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIDesignRequiresCompatibility"];
364
+ return value.boolValue;
365
+ }
366
+ return NO;
367
+ }
368
+
369
+ - (NSInteger)currentDetentIndex {
370
+ UISheetPresentationController *sheet = self.sheet;
371
+ if (!sheet)
372
+ return -1;
373
+
374
+ UISheetPresentationControllerDetentIdentifier selectedIdentifier = sheet.selectedDetentIdentifier;
375
+ if (!selectedIdentifier)
376
+ return -1;
377
+
378
+ NSArray<UISheetPresentationControllerDetent *> *detents = sheet.detents;
379
+ for (NSInteger i = 0; i < detents.count; i++) {
380
+ if (@available(iOS 16.0, *)) {
381
+ if ([detents[i].identifier isEqualToString:selectedIdentifier]) {
382
+ return i;
383
+ }
384
+ } else {
385
+ if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierMedium]) {
386
+ return 0;
387
+ } else if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierLarge]) {
388
+ return detents.count - 1;
389
+ }
390
+ }
391
+ }
392
+
393
+ return -1;
394
+ }
395
+
396
+ #pragma mark - View Lifecycle
397
+
398
+ - (void)loadView {
399
+ TrueSheetControllerView *view = [[TrueSheetControllerView alloc] init];
400
+ view.controller = self;
401
+ self.view = view;
87
402
  }
88
403
 
89
404
  - (void)viewDidLoad {
90
405
  [super viewDidLoad];
91
-
92
406
  self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
93
- _backgroundView.frame = self.view.bounds;
94
- [self.view insertSubview:self->_backgroundView atIndex:0];
407
+
408
+ _blurView = [[TrueSheetBlurView alloc] init];
409
+ [_blurView addToView:self.view];
410
+
411
+ _grabberView = [[TrueSheetGrabberView alloc] init];
412
+ _grabberView.hidden = YES;
413
+ [_grabberView addToView:self.view];
95
414
  }
96
415
 
97
416
  - (void)viewWillAppear:(BOOL)animated {
98
417
  [super viewWillAppear:animated];
99
418
 
100
- // Only trigger willPresent on the initial presentation, not on repositioning
419
+ _blurView.alpha = 1;
420
+
101
421
  if (!_isPresented) {
102
- if ([self.delegate respondsToSelector:@selector(viewControllerWillPresent)]) {
103
- [self.delegate viewControllerWillPresent];
422
+ UIViewController *presenter = self.presentingViewController;
423
+ if ([presenter isKindOfClass:[TrueSheetViewController class]]) {
424
+ _parentSheetController = (TrueSheetViewController *)presenter;
425
+ [_parentSheetController.delegate viewControllerWillBlur];
426
+ [_parentSheetController setAccessibilityContentElement:self.accessibilityContentView ?: self.view];
104
427
  }
105
428
 
106
- // Setup transition position tracking
107
- [self setupTransitionPositionTracking];
429
+ dispatch_async(dispatch_get_main_queue(), ^{
430
+ NSInteger index = self.currentDetentIndex;
431
+ CGFloat position = self.currentPosition;
432
+ CGFloat detent = [self detentValueForIndex:index];
433
+
434
+ [self.delegate viewControllerWillPresentAtIndex:index position:position detent:detent];
435
+ [self.delegate viewControllerWillFocus];
436
+ });
108
437
  }
438
+
439
+ [self setupTransitionTracker];
109
440
  }
110
441
 
111
442
  - (void)viewDidAppear:(BOOL)animated {
112
443
  [super viewDidAppear:animated];
444
+ [self setSheetAccessibilityElementsHidden:NO];
113
445
 
114
- // Only trigger didPresent on the initial presentation, not on repositioning
115
446
  if (!_isPresented) {
116
- if ([self.delegate respondsToSelector:@selector(viewControllerDidPresent)]) {
117
- [self.delegate viewControllerDidPresent];
118
- }
447
+ [_parentSheetController.delegate viewControllerDidBlur];
119
448
 
120
- // Setup gesture recognizer after view appears and React content is mounted
121
- [self setupGestureRecognizer];
449
+ dispatch_async(dispatch_get_main_queue(), ^{
450
+ NSInteger index = [self currentDetentIndex];
451
+ CGFloat detent = [self detentValueForIndex:index];
452
+ [self.delegate viewControllerDidPresentAtIndex:index position:self.currentPosition detent:detent];
453
+ [self.delegate viewControllerDidFocus];
122
454
 
455
+ [self->_grabberView updateAccessibilityValueWithIndex:index detentCount:self->_detents.count];
456
+ [self settleAtDetentIndex:index debug:@"did present"];
457
+ });
458
+
459
+ [self setupGestureRecognizer];
123
460
  _isPresented = YES;
124
461
  }
462
+
463
+ [self setupAccessibilityContainer];
464
+ }
465
+
466
+ - (void)setupAccessibilityContainer {
467
+ UIView *contentView = self.accessibilityContentView;
468
+ if (!contentView) {
469
+ return;
470
+ }
471
+
472
+ [self setAccessibilityContentElement:contentView];
125
473
  }
126
474
 
127
- - (void)viewWillDisappear:(BOOL)animated {
128
- [super viewWillDisappear:animated];
475
+ - (UIViewController *)accessibilityPresentingViewController {
476
+ UIViewController *presentingViewController = self.presentingViewController;
477
+ while ([presentingViewController isKindOfClass:[TrueSheetViewController class]] &&
478
+ presentingViewController.presentingViewController) {
479
+ presentingViewController = presentingViewController.presentingViewController;
480
+ }
481
+ return presentingViewController;
482
+ }
483
+
484
+ - (void)restoreWindowAccessibilityElements {
485
+ UIWindow *window = _accessibilityWindow;
486
+ if (window) {
487
+ // The active sheet may temporarily own window accessibility traversal. Only
488
+ // restore the previous value when this controller still owns that override.
489
+ NSValue *ownerValue = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey);
490
+ if (ownerValue && ownerValue.nonretainedObjectValue == self) {
491
+ id previousElements = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey);
492
+ window.accessibilityElements = previousElements == [NSNull null] ? nil : previousElements;
493
+ objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
494
+ objc_setAssociatedObject(
495
+ window, &TrueSheetAccessibilityWindowPreviousElementsKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
496
+ }
497
+
498
+ _accessibilityWindow = nil;
499
+ }
500
+ }
501
+
502
+ - (void)setSheetAccessibilityElementsHidden:(BOOL)hidden {
503
+ self.view.accessibilityElementsHidden = hidden;
504
+
505
+ UIView *presentedView = self.presentationController.presentedView;
506
+ if (presentedView) {
507
+ presentedView.accessibilityElementsHidden = hidden;
508
+ }
509
+ }
510
+
511
+ - (NSArray *)accessibilityGrabberElements {
512
+ if (_grabberView && !_grabberView.hidden) {
513
+ return @[ _grabberView ];
514
+ }
129
515
 
130
- if ([self.delegate respondsToSelector:@selector(viewControllerWillDismiss)]) {
516
+ // The system grabber is an accessibility element UIKit adds to the presented view
517
+ // (a sibling of our root view). Expose it so VoiceOver can resize or dismiss with it.
518
+ // UIKit also keeps a transparent bottom grabber there, so skip invisible views.
519
+ NSMutableArray *elements = [NSMutableArray array];
520
+ for (UIView *subview in self.presentationController.presentedView.subviews) {
521
+ if (subview != self.view && subview.isAccessibilityElement && !subview.hidden && subview.alpha > 0) {
522
+ [elements addObject:subview];
523
+ }
524
+ }
525
+ return elements;
526
+ }
527
+
528
+ - (BOOL)accessibilityPerformEscape {
529
+ if (!_isPresented) {
530
+ return NO;
531
+ }
532
+
533
+ if (!self.dismissible) {
534
+ [self.delegate viewControllerDidAttemptDismiss];
535
+ return NO;
536
+ }
537
+
538
+ [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
539
+ return YES;
540
+ }
541
+
542
+ - (void)setAccessibilityContentElement:(UIView *)contentView {
543
+ BOOL hasAccessibilityFooterElements = [contentView isKindOfClass:[TrueSheetContainerView class]] &&
544
+ [(TrueSheetContainerView *)contentView hasAccessibilityFooterElements];
545
+ // Footer controls exposed as separate accessibility elements can be skipped
546
+ // by XCTest when the presented sheet is a hard modal accessibility boundary.
547
+ BOOL isAccessibilityModal = _dimmed && !hasAccessibilityFooterElements;
548
+
549
+ // At a hard modal boundary XCTest skips nested elements, so flatten the
550
+ // container's children into the array. Otherwise expose the container itself:
551
+ // its accessibilityElements getter recomputes live, so a footer/content subtree
552
+ // that remounts (e.g. swapping a footer button on a state change) stays
553
+ // discoverable instead of leaving this snapshot pointing at destroyed views.
554
+ NSMutableArray *accessibilityElements = [[self accessibilityGrabberElements] mutableCopy];
555
+ if (isAccessibilityModal) {
556
+ NSArray *contentElements = contentView.accessibilityElements;
557
+ [accessibilityElements addObjectsFromArray:contentElements.count > 0 ? contentElements : @[ contentView ]];
558
+ } else {
559
+ [accessibilityElements addObject:contentView];
560
+ }
561
+
562
+ self.view.isAccessibilityElement = NO;
563
+ self.view.accessibilityViewIsModal = YES;
564
+ self.view.accessibilityElements = accessibilityElements;
565
+
566
+ UIView *presentedView = self.presentationController.presentedView;
567
+ if (presentedView) {
568
+ presentedView.isAccessibilityElement = NO;
569
+ presentedView.accessibilityViewIsModal = isAccessibilityModal;
570
+ presentedView.accessibilityElements = accessibilityElements;
571
+
572
+ // The system grabber is a sibling of our root view, so an escape performed while it
573
+ // is focused walks presentedView → window and never reaches TrueSheetControllerView.
574
+ if (@available(iOS 17.0, *)) {
575
+ __weak __typeof(self) weakSelf = self;
576
+ presentedView.accessibilityPerformEscapeBlock = ^BOOL {
577
+ return [weakSelf accessibilityPerformEscape];
578
+ };
579
+ }
580
+
581
+ UIWindow *window = self.view.window;
582
+ UIViewController *presentingViewController = [self accessibilityPresentingViewController];
583
+ if (window && presentingViewController.view) {
584
+ if (!_accessibilityWindow) {
585
+ _accessibilityWindow = window;
586
+ id previousElements = objc_getAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey);
587
+ if (!previousElements) {
588
+ previousElements = window.accessibilityElements ?: [NSNull null];
589
+ objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowPreviousElementsKey, previousElements,
590
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
591
+ }
592
+ }
593
+ objc_setAssociatedObject(window, &TrueSheetAccessibilityWindowOwnerKey, [NSValue valueWithNonretainedObject:self],
594
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
595
+ window.accessibilityElements =
596
+ isAccessibilityModal ? @[ presentedView ] : @[ presentingViewController.view, presentedView ];
597
+ return;
598
+ }
599
+ }
600
+
601
+ [self restoreWindowAccessibilityElements];
602
+ }
603
+
604
+ #pragma mark - Presentation
605
+
606
+ - (void)presentViewController:(UIViewController *)viewControllerToPresent
607
+ animated:(BOOL)flag
608
+ completion:(void (^)(void))completion {
609
+ // A non-sheet controller (e.g. an action sheet, alert, or image picker) is about
610
+ // to present over us. Clear our window accessibility override so UIKit's default
611
+ // traversal surfaces it for XCTest and assistive technologies; we reclaim the
612
+ // override when it dismisses. Nested sheets claim their own override.
613
+ if (![viewControllerToPresent isKindOfClass:[TrueSheetViewController class]]) {
614
+ self.view.window.accessibilityElements = nil;
615
+ }
616
+
617
+ [super presentViewController:viewControllerToPresent animated:flag completion:completion];
618
+ }
619
+
620
+ - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
621
+ [super dismissViewControllerAnimated:flag
622
+ completion:^{
623
+ if (completion) {
624
+ completion();
625
+ }
626
+
627
+ // Reclaim the window accessibility override once nothing
628
+ // else is presented over us.
629
+ if (self.isPresented && self.presentedViewController == nil) {
630
+ [self setupAccessibilityContainer];
631
+ }
632
+ }];
633
+ }
634
+
635
+ - (void)emitWillDismissEvents {
636
+ if (self.isBeingDismissed && !_isWillDismissEmitted) {
637
+ _isWillDismissEmitted = YES;
638
+
639
+ [self.delegate viewControllerWillBlur];
131
640
  [self.delegate viewControllerWillDismiss];
641
+ [_parentSheetController.delegate viewControllerWillFocus];
132
642
  }
643
+ }
133
644
 
134
- [self setupTransitionPositionTracking];
135
- _isTrackingPositionFromLayout = NO;
645
+ - (void)emitDidDismissEvents {
646
+ if (self.isBeingDismissed) {
647
+ [self restoreWindowAccessibilityElements];
648
+ _isPresented = NO;
649
+ _isWillDismissEmitted = NO;
650
+ _hasObservedBottomInset = NO;
651
+
652
+ [_anchorView removeFromSuperview];
653
+ _anchorView = nil;
654
+
655
+ [_parentSheetController.delegate viewControllerDidFocus];
656
+ [_parentSheetController setSheetAccessibilityElementsHidden:NO];
657
+ [_parentSheetController setupAccessibilityContainer];
658
+ _parentSheetController = nil;
659
+
660
+ [self.delegate viewControllerDidBlur];
661
+ [self.delegate viewControllerDidDismiss];
662
+ }
663
+ }
664
+
665
+ - (void)viewWillDisappear:(BOOL)animated {
666
+ [super viewWillDisappear:animated];
667
+ [self restoreWindowAccessibilityElements];
668
+ [self setSheetAccessibilityElementsHidden:YES];
669
+
670
+ // Dispatch to allow pan gesture to set _isDragging before checking;
671
+ // the transition tracker emits when the sheet is transitioning to dismiss
672
+ dispatch_async(dispatch_get_main_queue(), ^{
673
+ if (!self->_isDragging) {
674
+ [self emitWillDismissEvents];
675
+ }
676
+ });
677
+
678
+ [self setupTransitionTracker];
136
679
  }
137
680
 
138
681
  - (void)viewDidDisappear:(BOOL)animated {
139
682
  [super viewDidDisappear:animated];
140
- if ([self.delegate respondsToSelector:@selector(viewControllerDidDismiss)]) {
141
- [self.delegate viewControllerDidDismiss];
683
+
684
+ // Backstop: if the sheet is torn down mid-gesture (e.g. owning view deallocated),
685
+ // the observer can't reach us through its weak delegate, so end here to invalidate
686
+ // _interactivePositionLink — which otherwise retains this controller indefinitely.
687
+ if (_isInteractiveDismiss) {
688
+ [self endInteractiveDismissState];
142
689
  }
143
690
 
144
- _isTrackingPositionFromLayout = NO;
145
- _isPresented = NO;
146
- _activeDetentIndex = -1;
691
+ // Dismissing with the keyboard up skips the hide notification — don't let
692
+ // the stale flag block learning on the next present.
693
+ _keyboardSheetGrown = NO;
694
+
695
+ [self emitDidDismissEvents];
147
696
  }
148
697
 
698
+ - (void)viewWillLayoutSubviews {
699
+ [super viewWillLayoutSubviews];
700
+
701
+ // Skip during transitions and nav swipes — their trackers own position then.
702
+ if (_isTransitioning || _isInteractiveDismiss) {
703
+ return;
704
+ }
705
+
706
+ // Mid-drag the frame tracks the finger — never settle or learn from it;
707
+ // pending changes keep their flags and settle on the first post-drag layout.
708
+ if ((_pendingContentSizeChange || _pendingDetentsChange) && !_isDragging) {
709
+ _pendingContentSizeChange = NO;
710
+ _pendingDetentsChange = NO;
711
+ [self settleAtDetentIndex:self.currentDetentIndex debug:@"layout"];
712
+ } else {
713
+ // A child on top moves this sheet two ways: an animated collapse/restore
714
+ // step (presenting/dismissing) — emit the target non-realtime so JS
715
+ // animates to it — or direct per-frame re-layouts while the child drags,
716
+ // which stay realtime. Mid-drag no child is on top, so drags emit realtime.
717
+ BOOL realtime = self.presentedViewController == nil || !self.isPresentedViewAnimating;
718
+
719
+ // A layout pass at rest can nudge the frame by a sub-pixel amount
720
+ // (pixel-grid snapping after present), leaving the interpolated index
721
+ // slightly off the whole number. Re-learn when the frame is effectively
722
+ // at the current detent so the drift is absorbed instead of emitted.
723
+ // The tight threshold keeps real movement (e.g. scroll-driven expansion
724
+ // or a drag hovering near the detent) from being absorbed.
725
+ if (!_isDragging && self.presentedViewController == nil && !self.isPresentedViewAnimating) {
726
+ NSInteger index = self.currentDetentIndex;
727
+ CGFloat expectedHeight = [_detentCalculator resolvedHeightForIndex:index];
728
+ CGFloat actualHeight = self.screenHeight - self.currentPosition;
729
+ if (expectedHeight > 0 && fabs(actualHeight - expectedHeight) < 2) {
730
+ [self learnOffsetForDetentIndex:index];
731
+ }
732
+ }
733
+
734
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:realtime debug:@"layout"];
735
+ }
736
+ }
737
+
738
+ // Rotation or an iPad window resize changes the height fractional detents
739
+ // resolve against — re-resolve alongside the transition so the sheet lands on
740
+ // the new height instead of waiting for a content-driven rebuild.
149
741
  - (void)viewWillTransitionToSize:(CGSize)size
150
742
  withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
151
743
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
152
744
 
153
- // Handle rotation/size change
154
- [coordinator
155
- animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
156
- // Animation block - updates happen here
157
- }
158
- completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
159
- // After rotation completes
160
- [self setupSheetDetents];
161
-
162
- // Notify delegate of size changes
163
- if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeSize:)]) {
164
- [self.delegate viewControllerDidChangeSize:self.view.frame.size];
745
+ if (@available(iOS 16.0, *)) {
746
+ [coordinator
747
+ animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
748
+ [self.sheet invalidateDetents];
165
749
  }
166
- }];
750
+ completion:nil];
751
+ }
752
+ }
753
+
754
+ // Fires when UIKit resolves the sheet's float/anchored placement (present,
755
+ // detent resize crossing the float threshold, rotation). The footer's
756
+ // absorbed inset follows the observed safe area — repush it right away so
757
+ // the footer pads/unpads in the same layout pass.
758
+ - (void)viewSafeAreaInsetsDidChange {
759
+ [super viewSafeAreaInsetsDidChange];
760
+
761
+ // Mid-dismiss the frame moves off-screen and reports transient insets —
762
+ // don't let them pollute the observed value (matches viewDidLayoutSubviews).
763
+ if (!self.isBeingDismissed) {
764
+ [self.delegate viewControllerSafeAreaInsetsDidChange];
765
+ }
167
766
  }
168
767
 
169
768
  - (void)viewDidLayoutSubviews {
170
769
  [super viewDidLayoutSubviews];
171
- if (!_isTransitioning && self.isActiveAndVisible) {
172
- // Flag that we are tracking position from layout
173
- _isTrackingPositionFromLayout = YES;
174
-
175
- // If another controller is presented on top, treat position changes as transitioning
176
- // This prevents incorrect position notifications when overlays adjust our size
177
- [self emitChangePositionDelegateWithPosition:self.currentPosition
178
- transitioning:_layoutTransitioning || !self.isTopmostPresentedController];
179
-
180
- // On IOS 26, this is called twice when we have a ScrollView
181
- // Schedule flag reset after animation to avoid race condition
182
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
183
- // Reset layout transitioning after sending notification
184
- self->_layoutTransitioning = NO;
770
+
771
+ // viewSafeAreaInsetsDidChange doesn't fire on re-present when the observed
772
+ // inset matches the previous presentation's — refresh every layout pass
773
+ // instead (deduped downstream, so unchanged values are free).
774
+ if (!self.isBeingDismissed) {
775
+ [self.delegate viewControllerSafeAreaInsetsDidChange];
776
+ }
777
+
778
+ // Skip size reports while backgrounded behind a stacked child — the push-back
779
+ // scaling changes the frame and would needlessly resize content.
780
+ // _lastReportedSize stays stale so the restore pass re-reports any real change.
781
+ if (!self.isStackedBehindChild) {
782
+ // Report any size change (detent resize, keyboard, rotation) so Yoga
783
+ // relayouts the container synchronously with the sheet.
784
+ CGSize size = self.view.frame.size;
785
+ if (!CGSizeEqualToSize(_lastReportedSize, size)) {
786
+ _lastReportedSize = size;
787
+ [self.delegate viewControllerDidChangeSize:size];
788
+ }
789
+ }
790
+
791
+ if (_pendingDetentIndex >= 0) {
792
+ NSInteger pendingIndex = _pendingDetentIndex;
793
+ _pendingDetentIndex = -1;
794
+
795
+ // The presentedView frame isn't final until UIKit finishes the resize
796
+ // animation — no completion hook exists for it, hence the delay.
797
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
798
+ CGFloat detent = [self detentValueForIndex:pendingIndex];
799
+ [self.delegate viewControllerDidChangeDetent:pendingIndex position:self.currentPosition detent:detent];
800
+ [self->_grabberView updateAccessibilityValueWithIndex:pendingIndex detentCount:self->_detents.count];
801
+ [self settleAtDetentIndex:pendingIndex debug:@"pending detent change"];
185
802
  });
186
803
  }
187
804
  }
188
805
 
189
- #pragma mark - Gesture Handling
806
+ #pragma mark - Position & Gesture Handling
190
807
 
191
- /**
192
- * Finds the TrueSheetContentView in the hierarchy.
193
- *
194
- * @param view The presentedView to start searching from
195
- * @return The TrueSheetContentView found, or nil
196
- */
197
808
  - (TrueSheetContentView *)findContentView:(UIView *)view {
198
- // Check if this view itself is TrueSheetContentView
199
809
  if ([view isKindOfClass:[TrueSheetContentView class]]) {
200
810
  return (TrueSheetContentView *)view;
201
811
  }
202
812
 
203
- // Recursively search all subviews
204
813
  for (UIView *subview in view.subviews) {
205
814
  TrueSheetContentView *found = [self findContentView:subview];
206
815
  if (found) {
@@ -216,44 +825,67 @@
216
825
  if (!presentedView)
217
826
  return;
218
827
 
219
- // Attach to presented view's pan gestures (sheet's own drag gesture from UIKit)
828
+ if (!self.draggable) {
829
+ [GestureUtil setPanGesturesEnabled:NO forView:presentedView];
830
+ return;
831
+ }
832
+
220
833
  [GestureUtil attachPanGestureHandler:presentedView target:self selector:@selector(handlePanGesture:)];
221
834
 
222
- // Find and attach to the first ScrollView's pan gesture in the view hierarchy
223
- // This handles cases where the sheet content includes a ScrollView
224
835
  TrueSheetContentView *contentView = [self findContentView:presentedView];
225
836
  if (contentView) {
226
837
  RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView];
227
- if (scrollViewComponent) {
228
- // Access the internal UIScrollView via the scrollView property
229
- UIScrollView *scrollView = scrollViewComponent.scrollView;
230
- if (scrollView) {
231
- [GestureUtil attachPanGestureHandler:scrollView target:self selector:@selector(handlePanGesture:)];
232
- }
838
+ if (scrollViewComponent && scrollViewComponent.scrollView) {
839
+ [GestureUtil attachPanGestureHandler:scrollViewComponent.scrollView
840
+ target:self
841
+ selector:@selector(handlePanGesture:)];
233
842
  }
234
843
  }
235
844
  }
236
845
 
846
+ - (void)setupDraggable {
847
+ UIView *presentedView = self.presentedView;
848
+ if (!presentedView)
849
+ return;
850
+
851
+ [GestureUtil setPanGesturesEnabled:self.draggable forView:presentedView];
852
+ }
853
+
237
854
  - (void)handlePanGesture:(UIPanGestureRecognizer *)gesture {
238
- NSInteger index = [self currentDetentIndex];
855
+ NSInteger index = self.currentDetentIndex;
856
+ CGFloat detent = [self detentValueForIndex:index];
239
857
 
240
- if ([self.delegate respondsToSelector:@selector(viewControllerDidDrag:index:position:)]) {
241
- [self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition];
242
- }
858
+ [self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition detent:detent];
243
859
 
244
860
  switch (gesture.state) {
245
861
  case UIGestureRecognizerStateBegan:
246
862
  _isDragging = YES;
247
863
  break;
248
864
  case UIGestureRecognizerStateChanged:
249
- if (!_isTrackingPositionFromLayout)
250
- [self emitChangePositionDelegateWithPosition:self.currentPosition transitioning:NO];
865
+ // Layout owns the emit while the sheet resizes with the finger (a
866
+ // layout pass is already queued), and the transition tick owns it
867
+ // during an interactive dismiss. Translation-only movement (e.g.
868
+ // dragging down to the first detent) triggers neither — emit here as
869
+ // the backup.
870
+ if (!_isTransitioning && !self.presentedView.layer.needsLayout && !self.viewIfLoaded.layer.needsLayout) {
871
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
872
+ }
251
873
  break;
252
874
  case UIGestureRecognizerStateEnded:
253
- case UIGestureRecognizerStateCancelled:
875
+ case UIGestureRecognizerStateCancelled: {
876
+ if (!_isTransitioning) {
877
+ // Dispatch so UIKit picks the target detent first; the settle emit is
878
+ // non-realtime and JS animates alongside UIKit's snap spring.
879
+ dispatch_async(dispatch_get_main_queue(), ^{
880
+ NSInteger endIndex = self.currentDetentIndex;
881
+ [self->_grabberView updateAccessibilityValueWithIndex:endIndex detentCount:self->_detents.count];
882
+ [self settleAtDetentIndex:endIndex debug:@"drag end"];
883
+ });
884
+ }
885
+
254
886
  _isDragging = NO;
255
887
  break;
256
-
888
+ }
257
889
  default:
258
890
  break;
259
891
  }
@@ -262,165 +894,336 @@
262
894
  #pragma mark - Position Tracking
263
895
 
264
896
  /**
265
- * Emits position change to the delegate if the position has changed.
266
- * @param transitioning Whether the position change is part of a transition animation
897
+ * Tracks the sheet's live position through a present/dismiss transition. The
898
+ * link's lifetime is scoped to the transition coordinator: started alongside
899
+ * the transition, stopped in its completion — which fires for finished and
900
+ * cancelled (interactive) transitions alike.
267
901
  */
268
- - (void)emitChangePositionDelegateWithPosition:(CGFloat)position transitioning:(BOOL)transitioning {
269
- if (_lastPosition != position) {
270
- _lastPosition = position;
902
+ - (void)setupTransitionTracker {
903
+ id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator;
904
+
905
+ // A cancelled dismiss re-enters viewWillAppear with the same coordinator —
906
+ // the tracker from viewWillDisappear still owns it.
907
+ if (!coordinator || _isTransitioning) {
908
+ return;
909
+ }
271
910
 
272
- // Emit position change delegate
273
- NSInteger index = [self currentDetentIndex];
911
+ _isTransitioning = YES;
274
912
 
275
- if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:transitioning:)]) {
276
- [self.delegate viewControllerDidChangePosition:index position:position transitioning:transitioning];
913
+ // Learn the resolver-vs-actual offset before emitting transition positions so
914
+ // the interpolated index lands exactly on the target detent. The presented
915
+ // frame is already final here (UIKit animates the layer, not the frame).
916
+ if (!self.isBeingDismissed) {
917
+ [self learnOffsetForDetentIndex:self.currentDetentIndex];
918
+ }
919
+
920
+ __weak __typeof(self) weakSelf = self;
921
+ [coordinator
922
+ animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
923
+ __strong __typeof(weakSelf) strongSelf = weakSelf;
924
+ if (!strongSelf)
925
+ return;
926
+
927
+ [strongSelf->_transitionLink invalidate];
928
+ strongSelf->_transitionLink = [CADisplayLink displayLinkWithTarget:strongSelf
929
+ selector:@selector(handleTransitionTick)];
930
+ // Track at the display's native refresh rate — the default caps at 60Hz
931
+ // on ProMotion, dropping every other frame of a 120Hz animation.
932
+ strongSelf->_transitionLink.preferredFrameRateRange = CAFrameRateRangeMake(60, 120, 120);
933
+ [strongSelf->_transitionLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
934
+ }
935
+ completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
936
+ __strong __typeof(weakSelf) strongSelf = weakSelf;
937
+ if (!strongSelf)
938
+ return;
939
+
940
+ [strongSelf->_transitionLink invalidate];
941
+ strongSelf->_transitionLink = nil;
942
+ strongSelf->_isTransitioning = NO;
943
+
944
+ // Settle after a present, cancelled dismiss, or detent-snap transition.
945
+ // Delayed because the presentedView frame isn't final until UIKit
946
+ // completes its layout pass after the transition animation — learning
947
+ // here absorbs any sub-pixel drift since the earlier learns.
948
+ if (strongSelf->_isPresented && !strongSelf.isBeingDismissed) {
949
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
950
+ [strongSelf settleAtDetentIndex:strongSelf.currentDetentIndex debug:@"transition end"];
951
+ });
952
+ }
953
+ }];
954
+ }
955
+
956
+ - (void)handleTransitionTick {
957
+ // Also covers the interactive dismiss phase (finger down) — layout passes
958
+ // are skipped while transitioning, so the tick owns position there too.
959
+ CGFloat position = self.livePosition;
960
+
961
+ if (self.isBeingDismissed) {
962
+ // Emit only once the dismiss is committed: on release UIKit moves the
963
+ // model frame off-screen, while a cancelled drag rewinds it back to the
964
+ // detent (isBeingDismissed stays YES during the rewind).
965
+ if (self.currentPosition >= self.screenHeight) {
966
+ [self emitWillDismissEvents];
967
+ }
968
+
969
+ // Hide blur at the end of dismiss to prevent UIVisualEffectView
970
+ // from causing a flicker/flash at the bottom edge of the sheet.
971
+ if (self.screenHeight - position < 1) {
972
+ _blurView.alpha = 0;
277
973
  }
278
974
  }
975
+
976
+ [self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition"];
279
977
  }
280
978
 
281
979
  /**
282
- * Sets up position tracking during view controller transitions using a fake view.
283
- *
284
- * This approach uses a hidden "fake" view added to the container that animates
285
- * alongside the actual presented view. By observing the presentation layer of this
286
- * fake view's frame, we can track smooth position changes during the native transition
287
- * animation without manually animating in JavaScript.
288
- *
289
- * The display link fires at screen refresh rate, allowing us to emit position updates
290
- * that match the native animation curve, providing smooth synchronized updates to JS.
980
+ * The sheet is at rest at a detent: learn the resolver-vs-actual offset and
981
+ * emit the settled position. Only valid when the presentedView frame is final
982
+ * never mid-drag or mid-animation, where learning would store a bogus offset.
291
983
  */
292
- - (void)setupTransitionPositionTracking {
293
- if (self.transitionCoordinator != nil) {
294
- _isTransitioning = YES;
984
+ - (void)settleAtDetentIndex:(NSInteger)index debug:(NSString *)debug {
985
+ // Don't learn while a child sits on top — the deck transform skews the
986
+ // measured frame; the next unobstructed settle learns.
987
+ if (self.presentedViewController == nil) {
988
+ [self learnOffsetForDetentIndex:index];
989
+ }
295
990
 
296
- // Get the container view to add our fake transition view
297
- UIView *containerView = self.sheetPresentationController.containerView;
298
- UIView *presentedView = self.presentedView;
991
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:debug];
992
+ }
299
993
 
300
- if (!containerView || !presentedView)
301
- return;
994
+ #pragma mark - Interactive Navigation Dismiss
302
995
 
303
- CGRect frame = presentedView.frame;
996
+ - (void)beginInteractiveDismiss {
997
+ if (_isInteractiveDismiss) {
998
+ // A settle animation from a prior gesture is still running. Stop it and reset so
999
+ // this gesture tracks from a clean state; its stale completion is ignored via the
1000
+ // generation check in animateInteractiveContainerToTransform.
1001
+ [_interactiveContainerView.layer removeAllAnimations];
1002
+ [self endInteractiveDismissState];
1003
+ }
304
1004
 
305
- // Determine if presenting or dismissing to set correct start position
306
- BOOL isPresenting = self.isBeingPresented;
1005
+ _interactiveGeneration += 1;
1006
+ _isInteractiveDismiss = YES;
1007
+ _interactiveStartPosition = self.currentPosition;
1008
+ _interactiveContainerView = self.sheet.containerView;
307
1009
 
308
- // Set initial position based on transition type:
309
- // - Presenting: Start from bottom (containerHeight) and animate up to detent
310
- // - Dismissing: Start from current position and animate down to bottom
311
- if (isPresenting) {
312
- frame.origin.y = self.containerHeight;
313
- } else {
314
- frame.origin.y = presentedView.frame.origin.y;
315
- }
1010
+ // Emit position from the container's presentation layer for the whole gesture, so
1011
+ // both the direct-set drag and the settle animation report a live, smooth position.
1012
+ _interactivePositionLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(emitInteractivePosition)];
1013
+ _interactivePositionLink.preferredFrameRateRange = CAFrameRateRangeMake(60, 120, 120);
1014
+ [_interactivePositionLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
1015
+ }
316
1016
 
317
- // Set fake view's initial frame before transition starts
318
- _fakeTransitionView.frame = frame;
319
-
320
- auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
321
- // Add fake view to container so it participates in the transition
322
- [[context containerView] addSubview:self->_fakeTransitionView];
323
-
324
- // Animate fake view to the presented view's target position
325
- // UIKit will animate this with the same timing curve as the sheet
326
- CGRect finalFrame = presentedView.frame;
327
- finalFrame.origin.y = presentedView.frame.origin.y;
328
- self->_fakeTransitionView.frame = finalFrame;
329
-
330
- // Set our last transition position so we can check if
331
- // fake view's presentation layer has changed
332
- // This value will not change if the sheet is being repositioned
333
- // during a transition after a drag
334
- self->_lastTransitionPosition = finalFrame.origin.y;
335
-
336
- // Start display link to track position changes at screen refresh rate
337
- // This fires at 60-120Hz and reads from the presentation layer
338
- self->_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(trackTransitionPosition:)];
339
- [self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
340
- };
1017
+ - (void)emitInteractivePosition {
1018
+ CALayer *presentation = _interactiveContainerView.layer.presentationLayer;
1019
+ CGFloat offset = presentation ? presentation.affineTransform.ty : 0;
1020
+ [self emitChangePositionDelegateWithPosition:_interactiveStartPosition + offset realtime:YES debug:@"nav swipe"];
1021
+ }
341
1022
 
342
- [self.transitionCoordinator
343
- animateAlongsideTransition:animation
344
- completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
345
- // Clean up display link
346
- [self->_displayLink invalidate];
347
- self->_displayLink = nil;
1023
+ // Translate the presentation container, not the sheet's presentedView (whose transform
1024
+ // UISheetPresentationController owns for the floating-sheet inset). Only runs for undimmed
1025
+ // sheets a dimmed sheet's dimming view intercepts the edge-swipe so the pop never starts.
1026
+ - (void)updateInteractiveDismiss:(CGFloat)progress {
1027
+ if (!_isInteractiveDismiss) {
1028
+ return;
1029
+ }
1030
+ CGFloat clamped = fmin(1, fmax(0, progress));
1031
+ CGFloat dy = clamped * (self.screenHeight - _interactiveStartPosition);
1032
+ _interactiveContainerView.transform = CGAffineTransformMakeTranslation(0, dy);
1033
+ }
348
1034
 
349
- // Remove fake view from hierarchy
350
- [self->_fakeTransitionView removeFromSuperview];
351
- self->_isTransitioning = NO;
352
- }];
1035
+ - (void)cancelInteractiveDismissWithDuration:(NSTimeInterval)duration {
1036
+ if (!_isInteractiveDismiss) {
1037
+ return;
353
1038
  }
1039
+ [self animateInteractiveContainerToTransform:CGAffineTransformIdentity
1040
+ duration:duration
1041
+ allowUserInteraction:YES
1042
+ completion:nil];
354
1043
  }
355
1044
 
356
- - (void)trackTransitionPosition:(CADisplayLink *)displayLink {
357
- UIView *presentedView = self.presentedView;
1045
+ - (void)finishInteractiveDismissWithDuration:(NSTimeInterval)duration completion:(void (^)(void))completion {
1046
+ if (!_isInteractiveDismiss) {
1047
+ if (completion) {
1048
+ completion();
1049
+ }
1050
+ return;
1051
+ }
1052
+ // Leave the sheet translated off-screen; the caller tears it down non-animated
1053
+ // from here so there is no second slide.
1054
+ CGFloat dy = self.screenHeight - _interactiveStartPosition;
1055
+ [self animateInteractiveContainerToTransform:CGAffineTransformMakeTranslation(0, dy)
1056
+ duration:duration
1057
+ allowUserInteraction:NO
1058
+ completion:completion];
1059
+ }
1060
+
1061
+ - (void)animateInteractiveContainerToTransform:(CGAffineTransform)transform
1062
+ duration:(NSTimeInterval)duration
1063
+ allowUserInteraction:(BOOL)allowUserInteraction
1064
+ completion:(void (^)(void))completion {
1065
+ UIView *container = _interactiveContainerView;
1066
+ NSUInteger generation = _interactiveGeneration;
1067
+
1068
+ UIViewAnimationOptions options = UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionBeginFromCurrentState;
1069
+ if (allowUserInteraction) {
1070
+ options |= UIViewAnimationOptionAllowUserInteraction;
1071
+ }
1072
+
1073
+ [UIView animateWithDuration:fmax(duration, 0.2)
1074
+ delay:0
1075
+ options:options
1076
+ animations:^{
1077
+ container.transform = transform;
1078
+ }
1079
+ completion:^(BOOL finished) {
1080
+ // A newer gesture superseded this settle and owns teardown now.
1081
+ if (generation != self->_interactiveGeneration) {
1082
+ return;
1083
+ }
1084
+ [self endInteractiveDismissState];
1085
+ if (completion) {
1086
+ completion();
1087
+ }
1088
+ }];
1089
+ }
1090
+
1091
+ - (void)endInteractiveDismissState {
1092
+ _isInteractiveDismiss = NO;
1093
+ _interactiveContainerView = nil;
1094
+ _interactiveStartPosition = 0;
1095
+ [_interactivePositionLink invalidate];
1096
+ _interactivePositionLink = nil;
1097
+ }
1098
+
1099
+ - (void)emitChangePositionDelegateWithPosition:(CGFloat)position realtime:(BOOL)realtime debug:(NSString *)debug {
1100
+ UIViewController *presented = self.presentedViewController;
1101
+ if (presented) {
1102
+ UIModalPresentationStyle style = presented.modalPresentationStyle;
1103
+ if (style == UIModalPresentationFullScreen || style == UIModalPresentationOverFullScreen ||
1104
+ style == UIModalPresentationCurrentContext || style == UIModalPresentationOverCurrentContext) {
1105
+ return;
1106
+ }
1107
+ }
1108
+
1109
+ TrueSheetPositionState state = {
1110
+ .position = position,
1111
+ .detent = [self interpolatedDetentForPosition:position],
1112
+ .index = [self interpolatedIndexForPosition:position],
1113
+ };
1114
+
1115
+ // Settle (non-realtime) emits are authoritative and bypass the dedupe — a
1116
+ // learn right before them can correct the interpolated index by less than
1117
+ // the tolerance, and the corrected value must still reach JS.
1118
+ if (!realtime || !TrueSheetPositionStateEquals(_lastEmittedPositionState, state)) {
1119
+ _lastEmittedPositionState = state;
1120
+
1121
+ [self.delegate viewControllerDidChangePosition:state.index
1122
+ position:state.position
1123
+ detent:state.detent
1124
+ realtime:realtime];
1125
+ }
1126
+ }
358
1127
 
359
- if (_isDragging || !_fakeTransitionView || !presentedView) {
1128
+ - (void)learnOffsetForDetentIndex:(NSInteger)index {
1129
+ if (_keyboardSheetGrown) {
360
1130
  return;
361
1131
  }
1132
+ [_detentCalculator learnOffsetForDetentIndex:index];
1133
+ }
362
1134
 
363
- // Get the presentation layer which contains the in-flight animated values
364
- // Unlike the model layer (which has the final/target value), the presentation
365
- // layer reflects the current state during animation
366
- CALayer *presentationLayer = _fakeTransitionView.layer.presentationLayer;
1135
+ - (void)setKeyboardSheetGrown:(BOOL)keyboardSheetGrown {
1136
+ BOOL wasGrown = _keyboardSheetGrown;
1137
+ _keyboardSheetGrown = keyboardSheetGrown;
367
1138
 
368
- if (presentationLayer) {
369
- BOOL transitioning = NO;
370
- CGFloat position = presentationLayer.frame.origin.y;
1139
+ // Settles are skipped or measure a mid-animation frame while the keyboard
1140
+ // has the sheet grown (e.g. a drag-end during the shrink-back) — re-settle
1141
+ // at the resting detent once the keyboard is fully away.
1142
+ if (wasGrown && !keyboardSheetGrown && self.isPresented && !_isDragging) {
1143
+ [self settleAtDetentIndex:self.currentDetentIndex debug:@"keyboard settled"];
1144
+ }
1145
+ }
371
1146
 
372
- // Our last transition position is nearly the same as fake view's layer position
373
- // Sheet must've been repositioning after dragging at lowest detent
374
- if (fabs(_lastTransitionPosition - position) < FLT_EPSILON) {
375
- // Let's just flag it as transitioning to let JS manually animate
376
- transitioning = YES;
1147
+ - (BOOL)findSegmentForPosition:(CGFloat)position outIndex:(NSInteger *)outIndex outProgress:(CGFloat *)outProgress {
1148
+ return [_detentCalculator findSegmentForPosition:position outIndex:outIndex outProgress:outProgress];
1149
+ }
377
1150
 
378
- // Use the target presented view's frame position to animate
379
- position = presentedView.frame.origin.y;
380
- } else {
381
- // We are actually getting changes to our fake view's layer position
382
- // Just update our last transition position
383
- _lastTransitionPosition = position;
1151
+ - (CGFloat)interpolatedIndexForPosition:(CGFloat)position {
1152
+ return [_detentCalculator interpolatedIndexForPosition:position];
1153
+ }
1154
+
1155
+ - (CGFloat)interpolatedDetentForPosition:(CGFloat)position {
1156
+ return [_detentCalculator interpolatedDetentForPosition:position];
1157
+ }
1158
+
1159
+ - (CGFloat)detentValueForIndex:(NSInteger)index {
1160
+ return [_detentCalculator detentValueForIndex:index];
1161
+ }
1162
+
1163
+ #pragma mark - Sheet Configuration
1164
+
1165
+ /**
1166
+ * Applies a content/header/footer/peek size change to the already-built
1167
+ * detents. Auto and peek detents resolve lazily from snapshots, so this only
1168
+ * refreshes the snapshots and invalidates — never reassigns `sheet.detents`,
1169
+ * which would force UIKit to re-layout the whole presentation stack (visibly
1170
+ * perturbing a sheet behind this one).
1171
+ */
1172
+ - (void)setupSheetDetentsForSizeChange {
1173
+ if (@available(iOS 16.0, *)) {
1174
+ CGFloat autoHeight = [_detentCalculator autoHeight];
1175
+ CGFloat peekHeight = [_detentCalculator peekHeight];
1176
+
1177
+ if (fabs(autoHeight - _autoDetentHeight) < 0.5 && fabs(peekHeight - _peekDetentHeight) < 0.5) {
1178
+ return;
384
1179
  }
385
1180
 
386
- // Emit the current animated Y position to JS
387
- // This provides smooth position updates that match the native animation curve
388
- [self emitChangePositionDelegateWithPosition:position transitioning:transitioning];
1181
+ _autoDetentHeight = autoHeight;
1182
+ _peekDetentHeight = peekHeight;
1183
+
1184
+ [self.sheet animateChanges:^{
1185
+ self->_pendingContentSizeChange = YES;
1186
+ [self.sheet invalidateDetents];
1187
+ }];
389
1188
  }
1189
+ // iOS 15: detents are fixed medium/large — size changes can't affect them.
390
1190
  }
391
1191
 
392
- #pragma mark - Sheet Configuration (iOS 15+)
1192
+ - (void)setupSheetDetentsForDetentsChange {
1193
+ _pendingDetentsChange = YES;
1194
+ [self setupSheetDetents];
1195
+ }
393
1196
 
394
1197
  - (void)setupSheetDetents {
395
- UISheetPresentationController *sheet = self.sheetPresentationController;
396
- if (!sheet)
1198
+ UISheetPresentationController *sheet = self.sheet;
1199
+ if (!sheet) {
1200
+ RCTLogError(@"TrueSheet: sheetPresentationController is nil in setupSheetDetents");
397
1201
  return;
1202
+ }
398
1203
 
399
1204
  NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
1205
+ [_detentCalculator clearResolvedHeights];
400
1206
 
401
- // Subtract bottomInset from content height to account for safe area
402
- // This prevents iOS from adding extra bottom insets automatically
403
- CGFloat totalHeight = [self.contentHeight floatValue] - _bottomInset;
1207
+ _autoDetentHeight = [_detentCalculator autoHeight];
1208
+ _peekDetentHeight = [_detentCalculator peekHeight];
404
1209
 
405
1210
  for (NSInteger index = 0; index < self.detents.count; index++) {
406
1211
  id detent = self.detents[index];
407
- UISheetPresentationControllerDetent *sheetDetent = [self detentForValue:detent
408
- withHeight:totalHeight
409
- atIndex:index];
1212
+ UISheetPresentationControllerDetent *sheetDetent = [self detentForValue:detent atIndex:index];
410
1213
  [detents addObject:sheetDetent];
411
1214
  }
412
1215
 
1216
+ [_detentCalculator setDetentCount:self.detents.count];
413
1217
  sheet.detents = detents;
414
1218
 
415
- // Setup dimmed background
416
- if (self.dimmed && [self.dimmedIndex integerValue] == 0) {
1219
+ if (self.dimmed && [self.dimmedDetentIndex integerValue] == 0) {
417
1220
  sheet.largestUndimmedDetentIdentifier = nil;
418
1221
  } else {
419
1222
  sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
420
1223
 
421
1224
  if (@available(iOS 16.0, *)) {
422
- if (self.dimmed && self.dimmedIndex) {
423
- NSInteger dimmedIdx = [self.dimmedIndex integerValue];
1225
+ if (self.dimmed && self.dimmedDetentIndex) {
1226
+ NSInteger dimmedIdx = [self.dimmedDetentIndex integerValue];
424
1227
  if (dimmedIdx > 0 && dimmedIdx - 1 < sheet.detents.count) {
425
1228
  sheet.largestUndimmedDetentIdentifier = sheet.detents[dimmedIdx - 1].identifier;
426
1229
  } else if (sheet.detents.lastObject) {
@@ -433,62 +1236,109 @@
433
1236
  }
434
1237
  }
435
1238
 
436
- - (UISheetPresentationControllerDetent *)detentForValue:(id)detent withHeight:(CGFloat)height atIndex:(NSInteger)index {
1239
+ - (UISheetPresentationControllerDetent *)detentForValue:(id)detent atIndex:(NSInteger)index {
437
1240
  if (![detent isKindOfClass:[NSNumber class]]) {
438
1241
  return [UISheetPresentationControllerDetent mediumDetent];
439
1242
  }
440
1243
 
441
- CGFloat value = [detent floatValue];
1244
+ CGFloat value = [detent doubleValue];
442
1245
 
443
- // -1 represents "auto"
1246
+ // Auto/peek resolve from height snapshots — refreshed only in
1247
+ // setupSheetDetents and setupSheetDetentsForSizeChange — so size changes are
1248
+ // picked up on invalidation, while UIKit's spontaneous re-resolutions (e.g.
1249
+ // while a child sheet dismisses) read a stable value.
444
1250
  if (value == -1) {
445
1251
  if (@available(iOS 16.0, *)) {
446
- NSString *detentId = @"custom-auto";
447
- return [UISheetPresentationControllerDetent
448
- customDetentWithIdentifier:detentId
449
- resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
450
- CGFloat maxDetentValue = context.maximumDetentValue;
451
- CGFloat maxValue =
452
- self.maxHeight ? MIN(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
453
- CGFloat resolvedValue = MIN(height, maxValue);
454
- return resolvedValue;
455
- }];
1252
+ return [self customDetentWithIdentifier:@"custom-auto"
1253
+ atIndex:index
1254
+ heightBlock:^CGFloat {
1255
+ return self->_autoDetentHeight - [self autoDetentBottomInset];
1256
+ }];
1257
+ } else {
1258
+ return [UISheetPresentationControllerDetent mediumDetent];
1259
+ }
1260
+ }
1261
+
1262
+ if (value == -2) {
1263
+ if (@available(iOS 16.0, *)) {
1264
+ return [self customDetentWithIdentifier:@"custom-peek"
1265
+ atIndex:index
1266
+ heightBlock:^CGFloat {
1267
+ return self->_peekDetentHeight - [self peekDetentBottomInset];
1268
+ }];
456
1269
  } else {
457
1270
  return [UISheetPresentationControllerDetent mediumDetent];
458
1271
  }
459
1272
  }
460
1273
 
461
- // Handle fraction (0-1)
462
- // Fraction should only be > 0 and <= 1
463
1274
  if (value <= 0 || value > 1) {
464
1275
  RCTLogError(@"TrueSheet: detent fraction (%f) must be between 0 and 1", value);
465
1276
  return [UISheetPresentationControllerDetent mediumDetent];
466
1277
  }
467
1278
 
468
- NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
469
-
470
1279
  if (@available(iOS 16.0, *)) {
471
- return [UISheetPresentationControllerDetent
472
- customDetentWithIdentifier:detentId
473
- resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
474
- CGFloat maxDetentValue = context.maximumDetentValue;
475
- CGFloat maxValue =
476
- self.maxHeight ? MIN(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
477
- CGFloat resolvedValue = MIN(value * maxDetentValue, maxValue);
478
- return resolvedValue;
479
- }];
1280
+ NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
1281
+ // Resolve lazily — detents are built before presentation (no window yet), and
1282
+ // UIKit re-resolves on container size changes (rotation, windowed iPad resize).
1283
+ return [self customDetentWithIdentifier:detentId
1284
+ atIndex:index
1285
+ heightBlock:^CGFloat {
1286
+ return value * self.screenHeight;
1287
+ }];
1288
+ } else if (value >= 0.5) {
1289
+ return [UISheetPresentationControllerDetent largeDetent];
480
1290
  } else {
481
1291
  return [UISheetPresentationControllerDetent mediumDetent];
482
1292
  }
483
1293
  }
484
1294
 
1295
+ - (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
1296
+ atIndex:(NSInteger)index
1297
+ heightBlock:(CGFloat (^)(void))heightBlock
1298
+ API_AVAILABLE(ios(16.0)) {
1299
+ return [UISheetPresentationControllerDetent
1300
+ customDetentWithIdentifier:identifier
1301
+ resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
1302
+ CGFloat maxDetentValue = context.maximumDetentValue;
1303
+ self->_detentCalculator.maxDetentHeight = maxDetentValue;
1304
+
1305
+ CGFloat height = heightBlock();
1306
+ CGFloat bottomAdjustment = [self detentBottomAdjustmentForHeight:height];
1307
+ CGFloat maxValue = self.maxContentHeight
1308
+ ? fmin(maxDetentValue, [self.maxContentHeight floatValue])
1309
+ : maxDetentValue;
1310
+
1311
+ CGFloat adjustedHeight = height - bottomAdjustment;
1312
+
1313
+ // Only the last detent may occupy the full maximum. Two detents
1314
+ // resolving to the same height (e.g. a capped auto and 1) make
1315
+ // UIKit skip the stack's deck animation when presenting a child
1316
+ // sheet and snap the sheet behind into place instead.
1317
+ NSInteger lastIndex = (NSInteger)self.detents.count - 1;
1318
+ if (adjustedHeight >= maxValue && index < lastIndex) {
1319
+ maxValue -= lastIndex - index;
1320
+ }
1321
+
1322
+ CGFloat resolved = fmin(adjustedHeight, maxValue);
1323
+
1324
+ NSMutableArray *heights = self->_detentCalculator.resolvedDetentHeights;
1325
+ if (heights && index >= 0 && index < (NSInteger)heights.count) {
1326
+ heights[index] = @(resolved);
1327
+ }
1328
+
1329
+ return resolved;
1330
+ }];
1331
+ }
1332
+
485
1333
  - (UISheetPresentationControllerDetentIdentifier)detentIdentifierForIndex:(NSInteger)index {
486
- UISheetPresentationController *sheet = self.sheetPresentationController;
487
- if (!sheet)
1334
+ UISheetPresentationController *sheet = self.sheet;
1335
+ if (!sheet) {
1336
+ RCTLogError(@"TrueSheet: sheetPresentationController is nil in detentIdentifierForIndex");
488
1337
  return UISheetPresentationControllerDetentIdentifierMedium;
1338
+ }
489
1339
 
490
1340
  UISheetPresentationControllerDetentIdentifier identifier = UISheetPresentationControllerDetentIdentifierMedium;
491
- if (index < sheet.detents.count) {
1341
+ if (index >= 0 && index < (NSInteger)sheet.detents.count) {
492
1342
  UISheetPresentationControllerDetent *detent = sheet.detents[index];
493
1343
  if (@available(iOS 16.0, *)) {
494
1344
  identifier = detent.identifier;
@@ -503,17 +1353,15 @@
503
1353
  }
504
1354
 
505
1355
  - (void)applyActiveDetent {
506
- UISheetPresentationController *sheet = self.sheetPresentationController;
507
- if (!sheet)
1356
+ if (!self.sheet) {
1357
+ RCTLogError(@"TrueSheet: sheetPresentationController is nil in applyActiveDetent");
508
1358
  return;
1359
+ }
509
1360
 
510
- // Validate and clamp activeDetentIndex to detents bounds
511
1361
  NSInteger detentCount = _detents.count;
512
- if (detentCount == 0) {
1362
+ if (detentCount == 0)
513
1363
  return;
514
- }
515
1364
 
516
- // Clamp index to valid range
517
1365
  NSInteger clampedIndex = _activeDetentIndex;
518
1366
  if (clampedIndex < 0) {
519
1367
  clampedIndex = 0;
@@ -521,14 +1369,13 @@
521
1369
  clampedIndex = detentCount - 1;
522
1370
  }
523
1371
 
524
- // Update the stored index if it was clamped
525
1372
  if (clampedIndex != _activeDetentIndex) {
526
1373
  _activeDetentIndex = clampedIndex;
527
1374
  }
528
1375
 
529
1376
  UISheetPresentationControllerDetentIdentifier identifier = [self detentIdentifierForIndex:clampedIndex];
530
1377
  if (identifier) {
531
- sheet.selectedDetentIdentifier = identifier;
1378
+ self.sheet.selectedDetentIdentifier = identifier;
532
1379
  }
533
1380
  }
534
1381
 
@@ -537,113 +1384,251 @@
537
1384
  [self applyActiveDetent];
538
1385
  }
539
1386
 
540
- - (NSInteger)currentDetentIndex {
541
- UISheetPresentationController *sheet = self.sheetPresentationController;
542
- if (!sheet)
543
- return -1;
1387
+ - (void)resizeToDetentIndex:(NSInteger)index {
1388
+ if (index == _activeDetentIndex) {
1389
+ return;
1390
+ }
544
1391
 
545
- UISheetPresentationControllerDetentIdentifier selectedIdentifier = sheet.selectedDetentIdentifier;
546
- if (!selectedIdentifier)
547
- return -1;
1392
+ _pendingDetentIndex = index;
1393
+ _activeDetentIndex = index;
1394
+ [self applyActiveDetent];
1395
+ }
548
1396
 
549
- // Find the index by matching the identifier in the detents array
550
- NSArray<UISheetPresentationControllerDetent *> *detents = sheet.detents;
551
- for (NSInteger i = 0; i < detents.count; i++) {
552
- if (@available(iOS 16.0, *)) {
553
- if ([detents[i].identifier isEqualToString:selectedIdentifier]) {
554
- return i;
555
- }
556
- } else {
557
- // For iOS 15, we only support system detents (medium/large)
558
- // Return the index based on the selected identifier
559
- if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierMedium]) {
560
- return 0;
561
- } else if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierLarge]) {
562
- return detents.count - 1;
1397
+ - (void)setupBackground {
1398
+ auto effectiveBackgroundBlur = self.backgroundBlur;
1399
+ if (@available(iOS 26.0, *)) {
1400
+ // iOS 26+ has default liquid glass effect
1401
+ } else if (effectiveBackgroundBlur == TrueSheetViewBackgroundBlur::None && !self.backgroundColor) {
1402
+ effectiveBackgroundBlur = TrueSheetViewBackgroundBlur::SystemMaterial;
1403
+ }
1404
+
1405
+ BOOL hasBlur = effectiveBackgroundBlur != TrueSheetViewBackgroundBlur::None;
1406
+
1407
+ _blurView.backgroundBlur = hasBlur ? effectiveBackgroundBlur : TrueSheetViewBackgroundBlur::None;
1408
+ _blurView.blurIntensity = self.blurIntensity;
1409
+ _blurView.blurInteraction = self.blurInteraction;
1410
+ [_blurView applyBlurEffect];
1411
+
1412
+ #if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1) && !TARGET_OS_MACCATALYST
1413
+ if (@available(iOS 26.1, *)) {
1414
+ if (!self.isDesignCompatibilityMode) {
1415
+ if (self.backgroundColor || hasBlur) {
1416
+ self.sheet.backgroundEffect = [UIColorEffect effectWithColor:[UIColor clearColor]];
1417
+ } else {
1418
+ self.sheet.backgroundEffect = nil;
563
1419
  }
564
1420
  }
565
1421
  }
1422
+ #endif
566
1423
 
567
- return -1;
1424
+ self.view.backgroundColor = self.backgroundColor;
568
1425
  }
569
1426
 
570
- - (CGFloat)currentPosition {
571
- UIView *presentedView = self.presentedView;
572
- if (!presentedView)
573
- return 0.0;
1427
+ - (void)setupGrabber {
1428
+ BOOL showGrabber = self.grabber && self.draggable;
1429
+
1430
+ if (self.grabberOptions) {
1431
+ self.sheet.prefersGrabberVisible = NO;
1432
+
1433
+ GrabberOptions *options = self.grabberOptions;
1434
+ _grabberView.accessibilityOptions = self.accessibilityOptions;
1435
+ _grabberView.grabberWidth = options.width;
1436
+ _grabberView.grabberHeight = options.height;
1437
+ _grabberView.topMargin = options.topMargin;
1438
+ _grabberView.cornerRadius = options.cornerRadius;
1439
+ _grabberView.color = options.color;
1440
+ _grabberView.adaptive = @(options.adaptive);
1441
+ [_grabberView applyConfiguration];
1442
+ _grabberView.hidden = !showGrabber;
1443
+
1444
+ __weak __typeof(self) weakSelf = self;
1445
+ _grabberView.onTap = ^{
1446
+ [weakSelf handleGrabberTap];
1447
+ };
1448
+ _grabberView.onIncrement = ^{
1449
+ __strong __typeof(weakSelf) strongSelf = weakSelf;
1450
+ if (!strongSelf)
1451
+ return;
1452
+ NSInteger current = strongSelf.currentDetentIndex;
1453
+ NSInteger count = strongSelf->_detents.count;
1454
+ if (current >= 0 && current < count - 1) {
1455
+ [strongSelf.sheet animateChanges:^{
1456
+ [strongSelf resizeToDetentIndex:current + 1];
1457
+ }];
1458
+ }
1459
+ };
1460
+ _grabberView.onDecrement = ^{
1461
+ __strong __typeof(weakSelf) strongSelf = weakSelf;
1462
+ if (!strongSelf)
1463
+ return;
1464
+ NSInteger current = strongSelf.currentDetentIndex;
1465
+ if (current > 0) {
1466
+ [strongSelf.sheet animateChanges:^{
1467
+ [strongSelf resizeToDetentIndex:current - 1];
1468
+ }];
1469
+ } else if (strongSelf.dismissible) {
1470
+ [strongSelf.presentingViewController dismissViewControllerAnimated:YES completion:nil];
1471
+ } else {
1472
+ [strongSelf.delegate viewControllerDidAttemptDismiss];
1473
+ }
1474
+ };
574
1475
 
575
- return presentedView.frame.origin.y;
1476
+ [self.view bringSubviewToFront:_grabberView];
1477
+ } else {
1478
+ self.sheet.prefersGrabberVisible = showGrabber;
1479
+ _grabberView.hidden = YES;
1480
+ _grabberView.onTap = nil;
1481
+ _grabberView.onIncrement = nil;
1482
+ _grabberView.onDecrement = nil;
1483
+ }
1484
+
1485
+ // Only the topmost sheet may refresh, or a parent's prop update would steal the
1486
+ // window accessibility override from the child presented over it.
1487
+ if (_isPresented && self.isTopmostPresentedController) {
1488
+ [self setupAccessibilityContainer];
1489
+ }
576
1490
  }
577
1491
 
578
- - (CGFloat)currentHeight {
579
- return self.containerHeight - self.currentPosition - _bottomInset;
1492
+ - (void)handleGrabberTap {
1493
+ NSInteger detentCount = _detents.count;
1494
+ if (detentCount == 0)
1495
+ return;
1496
+
1497
+ NSInteger currentIndex = self.currentDetentIndex;
1498
+ if (currentIndex < 0)
1499
+ return;
1500
+
1501
+ NSInteger nextIndex = (currentIndex + 1) % detentCount;
1502
+ if (nextIndex == 0 && detentCount == 1) {
1503
+ if (self.dismissible) {
1504
+ [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
1505
+ } else {
1506
+ [self.delegate viewControllerDidAttemptDismiss];
1507
+ }
1508
+ } else {
1509
+ [self.sheet animateChanges:^{
1510
+ [self resizeToDetentIndex:nextIndex];
1511
+ }];
1512
+ }
580
1513
  }
581
1514
 
582
- - (CGFloat)containerHeight {
583
- UIView *containerView = self.sheetPresentationController.containerView;
584
- if (!containerView)
585
- return 0.0;
1515
+ - (BOOL)isAnchored {
1516
+ return self.anchor == TrueSheetViewAnchor::Left || self.anchor == TrueSheetViewAnchor::Right;
1517
+ }
1518
+
1519
+ - (void)setupAnchorViewInView:(UIView *)parentView {
1520
+ if (!parentView)
1521
+ return;
1522
+
1523
+ [_anchorView removeFromSuperview];
1524
+ _anchorView = nil;
1525
+
1526
+ if (!self.isAnchored) {
1527
+ self.sheetPresentationController.sourceView = nil;
1528
+ return;
1529
+ }
586
1530
 
587
- return containerView.frame.size.height;
1531
+ _anchorView = [[UIView alloc] init];
1532
+ _anchorView.userInteractionEnabled = NO;
1533
+ _anchorView.translatesAutoresizingMaskIntoConstraints = NO;
1534
+ [parentView addSubview:_anchorView];
1535
+
1536
+ NSLayoutAnchor *horizontalAnchor =
1537
+ self.anchor == TrueSheetViewAnchor::Right ? parentView.trailingAnchor : parentView.leadingAnchor;
1538
+
1539
+ [NSLayoutConstraint activateConstraints:@[
1540
+ [_anchorView.bottomAnchor constraintEqualToAnchor:parentView.bottomAnchor],
1541
+ [horizontalAnchor constraintEqualToAnchor:_anchorView.leadingAnchor],
1542
+ ]];
1543
+
1544
+ self.sheetPresentationController.sourceView = _anchorView;
588
1545
  }
589
1546
 
590
- - (void)setupSheetProps {
591
- UISheetPresentationController *sheet = self.sheetPresentationController;
592
- if (!sheet) {
1547
+ - (void)setupSheetSizing {
1548
+ UISheetPresentationController *sheet = self.sheet;
1549
+ if (!sheet)
593
1550
  return;
1551
+
1552
+ // `presentation` is absolute on the form side: 'form' always renders a
1553
+ // centered form sheet and ignores `maxContentWidth`. For 'page' with a
1554
+ // custom `maxContentWidth`, prefersPageSizing has to flip to NO since
1555
+ // `widthFollowsPreferredContentSizeWhenEdgeAttached` only takes effect
1556
+ // when the sheet is edge-attached (Apple API constraint).
1557
+ BOOL formSheet = self.presentation == facebook::react::TrueSheetViewPresentation::Form;
1558
+ BOOL hasMaxWidth = self.maxContentWidth != nil && !formSheet;
1559
+
1560
+ if (@available(iOS 17.0, *)) {
1561
+ sheet.prefersPageSizing = !formSheet && !hasMaxWidth;
594
1562
  }
595
1563
 
1564
+ sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = hasMaxWidth;
1565
+
1566
+ if (hasMaxWidth) {
1567
+ CGFloat height = self.maxContentHeight ? [self.maxContentHeight floatValue] : self.screenHeight;
1568
+ self.preferredContentSize = CGSizeMake([self.maxContentWidth floatValue], height);
1569
+ } else {
1570
+ self.preferredContentSize = CGSizeZero;
1571
+ }
1572
+ }
1573
+
1574
+ - (void)setupSheetProps {
1575
+ UISheetPresentationController *sheet = self.sheet;
1576
+ if (!sheet)
1577
+ return;
1578
+
596
1579
  sheet.delegate = self;
597
1580
  sheet.prefersEdgeAttachedInCompactHeight = YES;
598
- sheet.prefersGrabberVisible = self.grabber;
599
- // Only set preferredCornerRadius if explicitly provided, otherwise use system default
1581
+ sheet.prefersScrollingExpandsWhenScrolledToEdge = self.draggable && self.scrollingExpandsSheet;
1582
+
600
1583
  if (self.cornerRadius) {
601
1584
  sheet.preferredCornerRadius = [self.cornerRadius floatValue];
602
- }
603
-
604
- // Setup blur effect if blurTint is provided
605
- if (self.blurTint && self.blurTint.length > 0) {
606
- UIBlurEffectStyle style = UIBlurEffectStyleLight;
607
-
608
- if ([self.blurTint isEqualToString:@"dark"]) {
609
- style = UIBlurEffectStyleDark;
610
- } else if ([self.blurTint isEqualToString:@"light"]) {
611
- style = UIBlurEffectStyleLight;
612
- } else if ([self.blurTint isEqualToString:@"extraLight"]) {
613
- style = UIBlurEffectStyleExtraLight;
614
- } else if ([self.blurTint isEqualToString:@"regular"]) {
615
- style = UIBlurEffectStyleRegular;
616
- } else if ([self.blurTint isEqualToString:@"prominent"]) {
617
- style = UIBlurEffectStyleProminent;
618
- } else if ([self.blurTint isEqualToString:@"systemThinMaterial"]) {
619
- style = UIBlurEffectStyleSystemThinMaterial;
620
- } else if ([self.blurTint isEqualToString:@"systemMaterial"]) {
621
- style = UIBlurEffectStyleSystemMaterial;
622
- } else if ([self.blurTint isEqualToString:@"systemThickMaterial"]) {
623
- style = UIBlurEffectStyleSystemThickMaterial;
624
- } else if ([self.blurTint isEqualToString:@"systemChromeMaterial"]) {
625
- style = UIBlurEffectStyleSystemChromeMaterial;
626
- } else if ([self.blurTint isEqualToString:@"systemUltraThinMaterial"]) {
627
- style = UIBlurEffectStyleSystemUltraThinMaterial;
628
- }
629
-
630
- _backgroundView.effect = [UIBlurEffect effectWithStyle:style];
631
- _backgroundView.backgroundColor = nil;
632
1585
  } else {
633
- // No blur effect, use solid background color
634
- _backgroundView.effect = nil;
635
- _backgroundView.backgroundColor = self.backgroundColor;
1586
+ sheet.preferredCornerRadius = UISheetPresentationControllerAutomaticDimension;
636
1587
  }
1588
+
1589
+ [self setupBackground];
1590
+ [self setupGrabber];
637
1591
  }
638
1592
 
639
1593
  #pragma mark - UISheetPresentationControllerDelegate
640
1594
 
1595
+ - (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController {
1596
+ return self.dismissible;
1597
+ }
1598
+
1599
+ // UIKit calls this after shouldDismiss returns NO — the sheet has already
1600
+ // bounced back, so JS can confirm and dismiss programmatically (Apple's
1601
+ // unsaved-changes pattern).
1602
+ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController {
1603
+ [self.delegate viewControllerDidAttemptDismiss];
1604
+ }
1605
+
641
1606
  - (void)sheetPresentationControllerDidChangeSelectedDetentIdentifier:
642
1607
  (UISheetPresentationController *)sheetPresentationController {
643
- NSInteger index = [self currentDetentIndex];
644
- if (index >= 0 && [self.delegate respondsToSelector:@selector(viewControllerDidChangeDetent:position:)]) {
645
- [self.delegate viewControllerDidChangeDetent:index position:self.currentPosition];
1608
+ dispatch_async(dispatch_get_main_queue(), ^{
1609
+ NSInteger index = self.currentDetentIndex;
1610
+ if (index >= 0) {
1611
+ CGFloat detent = [self detentValueForIndex:index];
1612
+ [self.delegate viewControllerDidChangeDetent:index position:self.currentPosition detent:detent];
1613
+ }
1614
+ });
1615
+ }
1616
+
1617
+ #pragma mark - RNSDismissibleModalProtocol
1618
+
1619
+ #if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
1620
+ - (BOOL)isDismissible {
1621
+ return NO;
1622
+ }
1623
+
1624
+ - (UIViewController *)newPresentingViewController {
1625
+ UIViewController *topmost = self;
1626
+ while (topmost.presentedViewController != nil && !topmost.presentedViewController.isBeingDismissed &&
1627
+ [topmost.presentedViewController isKindOfClass:[TrueSheetViewController class]]) {
1628
+ topmost = topmost.presentedViewController;
646
1629
  }
1630
+ return topmost;
647
1631
  }
1632
+ #endif
648
1633
 
649
1634
  @end