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

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