@lodev09/react-native-true-sheet 2.0.6 → 3.0.0-beta.1

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 (194) hide show
  1. package/README.md +36 -8
  2. package/RNTrueSheet.podspec +20 -0
  3. package/android/build.gradle +26 -14
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +108 -0
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerViewManager.kt +21 -0
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +46 -0
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentViewManager.kt +21 -0
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +47 -0
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterViewManager.kt +21 -0
  10. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +165 -0
  11. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +36 -4
  12. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +257 -303
  13. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +855 -0
  14. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +104 -82
  15. package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +26 -0
  16. package/android/src/main/java/com/lodev09/truesheet/events/DidDismissEvent.kt +20 -0
  17. package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +26 -0
  18. package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +26 -0
  19. package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +26 -0
  20. package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +26 -0
  21. package/android/src/main/java/com/lodev09/truesheet/events/MountEvent.kt +20 -0
  22. package/android/src/main/java/com/lodev09/truesheet/events/PositionChangeEvent.kt +32 -0
  23. package/android/src/main/java/com/lodev09/truesheet/events/SizeChangeEvent.kt +27 -0
  24. package/android/src/main/java/com/lodev09/truesheet/events/WillDismissEvent.kt +20 -0
  25. package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +26 -0
  26. package/android/src/main/java/com/lodev09/truesheet/{core/Utils.kt → utils/ScreenUtils.kt} +47 -17
  27. package/android/src/main/res/values/styles.xml +8 -0
  28. package/ios/TrueSheetComponentDescriptor.h +24 -0
  29. package/ios/TrueSheetContainerView.h +47 -0
  30. package/ios/TrueSheetContainerView.mm +117 -0
  31. package/ios/TrueSheetContentView.h +37 -0
  32. package/ios/TrueSheetContentView.mm +114 -0
  33. package/ios/TrueSheetFooterView.h +27 -0
  34. package/ios/TrueSheetFooterView.mm +101 -0
  35. package/ios/TrueSheetModule.h +44 -0
  36. package/ios/TrueSheetModule.mm +133 -0
  37. package/ios/TrueSheetView.h +53 -0
  38. package/ios/TrueSheetView.mm +437 -0
  39. package/ios/TrueSheetViewController.h +65 -0
  40. package/ios/TrueSheetViewController.mm +672 -0
  41. package/ios/events/OnDetentChangeEvent.h +28 -0
  42. package/ios/events/OnDetentChangeEvent.mm +30 -0
  43. package/ios/events/OnDidDismissEvent.h +26 -0
  44. package/ios/events/OnDidDismissEvent.mm +25 -0
  45. package/ios/events/OnDidPresentEvent.h +28 -0
  46. package/ios/events/OnDidPresentEvent.mm +30 -0
  47. package/ios/events/OnDragBeginEvent.h +28 -0
  48. package/ios/events/OnDragBeginEvent.mm +30 -0
  49. package/ios/events/OnDragChangeEvent.h +28 -0
  50. package/ios/events/OnDragChangeEvent.mm +30 -0
  51. package/ios/events/OnDragEndEvent.h +28 -0
  52. package/ios/events/OnDragEndEvent.mm +30 -0
  53. package/ios/events/OnMountEvent.h +26 -0
  54. package/ios/events/OnMountEvent.mm +25 -0
  55. package/ios/events/OnPositionChangeEvent.h +29 -0
  56. package/ios/events/OnPositionChangeEvent.mm +32 -0
  57. package/ios/events/OnSizeChangeEvent.h +28 -0
  58. package/ios/events/OnSizeChangeEvent.mm +30 -0
  59. package/ios/events/OnWillDismissEvent.h +26 -0
  60. package/ios/events/OnWillDismissEvent.mm +25 -0
  61. package/ios/events/OnWillPresentEvent.h +28 -0
  62. package/ios/events/OnWillPresentEvent.mm +30 -0
  63. package/ios/utils/GestureUtil.h +25 -0
  64. package/ios/utils/GestureUtil.mm +26 -0
  65. package/ios/utils/LayoutUtil.h +44 -0
  66. package/ios/utils/LayoutUtil.mm +50 -0
  67. package/ios/utils/WindowUtil.h +27 -0
  68. package/ios/utils/WindowUtil.mm +42 -0
  69. package/lib/module/TrueSheet.js +231 -135
  70. package/lib/module/TrueSheet.js.map +1 -1
  71. package/lib/module/TrueSheetGrabber.js +16 -14
  72. package/lib/module/TrueSheetGrabber.js.map +1 -1
  73. package/lib/module/fabric/TrueSheetContainerViewNativeComponent.ts +8 -0
  74. package/lib/module/fabric/TrueSheetContentViewNativeComponent.ts +8 -0
  75. package/lib/module/fabric/TrueSheetFooterViewNativeComponent.ts +8 -0
  76. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +63 -0
  77. package/lib/module/index.js +1 -0
  78. package/lib/module/index.js.map +1 -1
  79. package/lib/module/reanimated/ReanimatedTrueSheet.js +87 -0
  80. package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -0
  81. package/lib/module/reanimated/ReanimatedTrueSheetProvider.js +72 -0
  82. package/lib/module/reanimated/ReanimatedTrueSheetProvider.js.map +1 -0
  83. package/lib/module/reanimated/index.js +6 -0
  84. package/lib/module/reanimated/index.js.map +1 -0
  85. package/lib/module/reanimated/useReanimatedPositionChangeHandler.js +19 -0
  86. package/lib/module/reanimated/useReanimatedPositionChangeHandler.js.map +1 -0
  87. package/lib/module/specs/NativeTrueSheetModule.js +12 -0
  88. package/lib/module/specs/NativeTrueSheetModule.js.map +1 -0
  89. package/lib/typescript/package.json +1 -0
  90. package/lib/typescript/src/TrueSheet.d.ts +79 -0
  91. package/lib/typescript/src/TrueSheet.d.ts.map +1 -0
  92. package/lib/typescript/src/TrueSheet.types.d.ts +260 -0
  93. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -0
  94. package/lib/typescript/{commonjs/src → src}/TrueSheetGrabber.d.ts +1 -1
  95. package/lib/typescript/src/TrueSheetGrabber.d.ts.map +1 -0
  96. package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts +6 -0
  97. package/lib/typescript/src/fabric/TrueSheetContainerViewNativeComponent.d.ts.map +1 -0
  98. package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts +6 -0
  99. package/lib/typescript/src/fabric/TrueSheetContentViewNativeComponent.d.ts.map +1 -0
  100. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts +6 -0
  101. package/lib/typescript/src/fabric/TrueSheetFooterViewNativeComponent.d.ts.map +1 -0
  102. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +44 -0
  103. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -0
  104. package/lib/typescript/{commonjs/src → src}/index.d.ts +1 -0
  105. package/lib/typescript/src/index.d.ts.map +1 -0
  106. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts +43 -0
  107. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts.map +1 -0
  108. package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts +57 -0
  109. package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts.map +1 -0
  110. package/lib/typescript/src/reanimated/index.d.ts +4 -0
  111. package/lib/typescript/src/reanimated/index.d.ts.map +1 -0
  112. package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.d.ts +6 -0
  113. package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.d.ts.map +1 -0
  114. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts +34 -0
  115. package/lib/typescript/src/specs/NativeTrueSheetModule.d.ts.map +1 -0
  116. package/package.json +104 -75
  117. package/react-native.config.js +17 -0
  118. package/src/TrueSheet.tsx +285 -188
  119. package/src/TrueSheet.types.ts +119 -106
  120. package/src/TrueSheetGrabber.tsx +29 -28
  121. package/src/__mocks__/index.js +60 -12
  122. package/src/fabric/TrueSheetContainerViewNativeComponent.ts +8 -0
  123. package/src/fabric/TrueSheetContentViewNativeComponent.ts +8 -0
  124. package/src/fabric/TrueSheetFooterViewNativeComponent.ts +8 -0
  125. package/src/fabric/TrueSheetViewNativeComponent.ts +63 -0
  126. package/src/index.ts +4 -3
  127. package/src/reanimated/ReanimatedTrueSheet.tsx +95 -0
  128. package/src/reanimated/ReanimatedTrueSheetProvider.tsx +92 -0
  129. package/src/reanimated/index.ts +3 -0
  130. package/src/reanimated/useReanimatedPositionChangeHandler.ts +26 -0
  131. package/src/specs/NativeTrueSheetModule.ts +38 -0
  132. package/TrueSheet.podspec +0 -49
  133. package/android/src/main/java/com/lodev09/truesheet/TrueSheetDialog.kt +0 -413
  134. package/android/src/main/java/com/lodev09/truesheet/TrueSheetEvent.kt +0 -22
  135. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewModule.kt +0 -63
  136. package/android/src/main/java/com/lodev09/truesheet/core/KeyboardManager.kt +0 -58
  137. package/android/src/main/java/com/lodev09/truesheet/core/RootSheetView.kt +0 -102
  138. package/ios/Extensions/UIBlurEffect+withTint.swift +0 -62
  139. package/ios/Extensions/UIView+pinTo.swift +0 -74
  140. package/ios/Extensions/UIViewController+detentForSize.swift +0 -134
  141. package/ios/TrueSheet-Bridging-Header.h +0 -14
  142. package/ios/TrueSheetEvent.swift +0 -48
  143. package/ios/TrueSheetView.swift +0 -467
  144. package/ios/TrueSheetViewController.swift +0 -275
  145. package/ios/TrueSheetViewManager.m +0 -53
  146. package/ios/TrueSheetViewManager.swift +0 -48
  147. package/ios/Utils/Logger.swift +0 -39
  148. package/ios/Utils/Promise.swift +0 -25
  149. package/lib/commonjs/TrueSheet.js +0 -258
  150. package/lib/commonjs/TrueSheet.js.map +0 -1
  151. package/lib/commonjs/TrueSheet.types.js +0 -6
  152. package/lib/commonjs/TrueSheet.types.js.map +0 -1
  153. package/lib/commonjs/TrueSheetFooter.js +0 -19
  154. package/lib/commonjs/TrueSheetFooter.js.map +0 -1
  155. package/lib/commonjs/TrueSheetGrabber.js +0 -54
  156. package/lib/commonjs/TrueSheetGrabber.js.map +0 -1
  157. package/lib/commonjs/TrueSheetModule.js +0 -19
  158. package/lib/commonjs/TrueSheetModule.js.map +0 -1
  159. package/lib/commonjs/__mocks__/index.js +0 -52
  160. package/lib/commonjs/__mocks__/index.js.map +0 -1
  161. package/lib/commonjs/index.js +0 -39
  162. package/lib/commonjs/index.js.map +0 -1
  163. package/lib/module/TrueSheetFooter.js +0 -14
  164. package/lib/module/TrueSheetFooter.js.map +0 -1
  165. package/lib/module/TrueSheetModule.js +0 -15
  166. package/lib/module/TrueSheetModule.js.map +0 -1
  167. package/lib/module/__mocks__/index.js +0 -21
  168. package/lib/module/__mocks__/index.js.map +0 -1
  169. package/lib/typescript/commonjs/package.json +0 -1
  170. package/lib/typescript/commonjs/src/TrueSheet.d.ts +0 -70
  171. package/lib/typescript/commonjs/src/TrueSheet.d.ts.map +0 -1
  172. package/lib/typescript/commonjs/src/TrueSheet.types.d.ts +0 -241
  173. package/lib/typescript/commonjs/src/TrueSheet.types.d.ts.map +0 -1
  174. package/lib/typescript/commonjs/src/TrueSheetFooter.d.ts +0 -7
  175. package/lib/typescript/commonjs/src/TrueSheetFooter.d.ts.map +0 -1
  176. package/lib/typescript/commonjs/src/TrueSheetGrabber.d.ts.map +0 -1
  177. package/lib/typescript/commonjs/src/TrueSheetModule.d.ts +0 -2
  178. package/lib/typescript/commonjs/src/TrueSheetModule.d.ts.map +0 -1
  179. package/lib/typescript/commonjs/src/index.d.ts.map +0 -1
  180. package/lib/typescript/module/src/TrueSheet.d.ts +0 -70
  181. package/lib/typescript/module/src/TrueSheet.d.ts.map +0 -1
  182. package/lib/typescript/module/src/TrueSheet.types.d.ts +0 -241
  183. package/lib/typescript/module/src/TrueSheet.types.d.ts.map +0 -1
  184. package/lib/typescript/module/src/TrueSheetFooter.d.ts +0 -7
  185. package/lib/typescript/module/src/TrueSheetFooter.d.ts.map +0 -1
  186. package/lib/typescript/module/src/TrueSheetGrabber.d.ts +0 -39
  187. package/lib/typescript/module/src/TrueSheetGrabber.d.ts.map +0 -1
  188. package/lib/typescript/module/src/TrueSheetModule.d.ts +0 -2
  189. package/lib/typescript/module/src/TrueSheetModule.d.ts.map +0 -1
  190. package/lib/typescript/module/src/index.d.ts +0 -4
  191. package/lib/typescript/module/src/index.d.ts.map +0 -1
  192. package/src/TrueSheetFooter.tsx +0 -17
  193. package/src/TrueSheetModule.ts +0 -19
  194. /package/lib/{typescript/module → module}/package.json +0 -0
@@ -0,0 +1,672 @@
1
+ //
2
+ // Created by Jovanni Lo (@lodev09)
3
+ // Copyright (c) 2024-present. All rights reserved.
4
+ //
5
+ // This source code is licensed under the MIT license found in the
6
+ // LICENSE file in the root directory of this source tree.
7
+ //
8
+
9
+ #import "TrueSheetViewController.h"
10
+ #import "utils/WindowUtil.h"
11
+
12
+ #import <React/RCTLog.h>
13
+ #import <React/RCTScrollViewComponentView.h>
14
+ #import "TrueSheetContentView.h"
15
+ #import "utils/GestureUtil.h"
16
+
17
+ @interface TrueSheetViewController ()
18
+
19
+ @end
20
+
21
+ @implementation TrueSheetViewController {
22
+ CGFloat _lastPosition;
23
+ CGFloat _lastTransitionPosition;
24
+ UIVisualEffectView *_backgroundView;
25
+ CGFloat _bottomInset;
26
+ BOOL _isTransitioning;
27
+ BOOL _isDragging;
28
+ BOOL _isTrackingPositionFromLayout;
29
+ UIView *_fakeTransitionView;
30
+ CADisplayLink *_displayLink;
31
+ }
32
+
33
+ - (instancetype)init {
34
+ if (self = [super initWithNibName:nil bundle:nil]) {
35
+ _detents = @[ @0.5, @1 ];
36
+ _contentHeight = @(0);
37
+ _grabber = YES;
38
+ _dimmed = YES;
39
+ _dimmedIndex = @(0);
40
+ _lastPosition = 0;
41
+ _lastTransitionPosition = 0;
42
+ _isTransitioning = NO;
43
+ _isDragging = NO;
44
+ _isTrackingPositionFromLayout = NO;
45
+ _layoutTransitioning = NO;
46
+ _isPresented = NO;
47
+ _activeDetentIndex = -1;
48
+
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;
61
+
62
+ // Allow modals to be presented from this view controller
63
+ self.definesPresentationContext = YES;
64
+ }
65
+ return self;
66
+ }
67
+
68
+ - (void)dealloc {
69
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
70
+ }
71
+
72
+ #pragma mark - Presentation State
73
+
74
+ - (BOOL)isTopmostPresentedController {
75
+ // Check if we're in the window hierarchy and visible
76
+ if (!self.isViewLoaded || self.view.window == nil) {
77
+ return NO;
78
+ }
79
+
80
+ // Check if another controller is presented on top of this sheet
81
+ return self.presentedViewController == nil;
82
+ }
83
+
84
+ - (BOOL)isActiveAndVisible {
85
+ return self.isViewLoaded && self.view.window != nil && !self.isBeingDismissed;
86
+ }
87
+
88
+ - (UIView *)presentedView {
89
+ return self.sheetPresentationController.presentedView;
90
+ }
91
+
92
+ - (void)viewDidLoad {
93
+ [super viewDidLoad];
94
+
95
+ self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
96
+ _backgroundView.frame = self.view.bounds;
97
+ [self.view insertSubview:self->_backgroundView atIndex:0];
98
+ }
99
+
100
+ - (void)viewWillAppear:(BOOL)animated {
101
+ [super viewWillAppear:animated];
102
+
103
+ // Only trigger willPresent on the initial presentation, not on repositioning
104
+ if (!_isPresented) {
105
+ if ([self.delegate respondsToSelector:@selector(viewControllerWillPresent)]) {
106
+ [self.delegate viewControllerWillPresent];
107
+ }
108
+
109
+ // Setup transition position tracking
110
+ [self setupTransitionPositionTracking];
111
+ }
112
+ }
113
+
114
+ - (void)viewDidAppear:(BOOL)animated {
115
+ [super viewDidAppear:animated];
116
+
117
+ // Only trigger didPresent on the initial presentation, not on repositioning
118
+ if (!_isPresented) {
119
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidPresent)]) {
120
+ [self.delegate viewControllerDidPresent];
121
+ }
122
+
123
+ // Setup gesture recognizer after view appears and React content is mounted
124
+ [self setupGestureRecognizer];
125
+
126
+ _isPresented = YES;
127
+ }
128
+ }
129
+
130
+ - (void)viewWillDisappear:(BOOL)animated {
131
+ [super viewWillDisappear:animated];
132
+
133
+ // Only dispatch willDismiss if the sheet is actually being dismissed
134
+ if (self.isBeingDismissed && [self.delegate respondsToSelector:@selector(viewControllerWillDismiss)]) {
135
+ [self.delegate viewControllerWillDismiss];
136
+ }
137
+
138
+ [self setupTransitionPositionTracking];
139
+ _isTrackingPositionFromLayout = NO;
140
+ }
141
+
142
+ - (void)viewDidDisappear:(BOOL)animated {
143
+ [super viewDidDisappear:animated];
144
+
145
+ // Only dispatch didDismiss if the sheet is actually being dismissed
146
+ // (not when another modal is presented on top)
147
+ BOOL isActuallyDismissing = self.presentingViewController == nil || self.isBeingDismissed;
148
+
149
+ if (isActuallyDismissing && [self.delegate respondsToSelector:@selector(viewControllerDidDismiss)]) {
150
+ [self.delegate viewControllerDidDismiss];
151
+ }
152
+
153
+ _isTrackingPositionFromLayout = NO;
154
+
155
+ // Only reset state if actually dismissing
156
+ if (isActuallyDismissing) {
157
+ _isPresented = NO;
158
+ _activeDetentIndex = -1;
159
+ }
160
+ }
161
+
162
+ - (void)viewWillTransitionToSize:(CGSize)size
163
+ withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
164
+ [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
165
+
166
+ // Handle rotation/size change
167
+ [coordinator
168
+ animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
169
+ // Animation block - updates happen here
170
+ }
171
+ completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
172
+ // After rotation completes
173
+ [self setupSheetDetents];
174
+
175
+ // Notify delegate of size changes
176
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeSize:)]) {
177
+ [self.delegate viewControllerDidChangeSize:self.view.frame.size];
178
+ }
179
+ }];
180
+ }
181
+
182
+ - (void)viewDidLayoutSubviews {
183
+ [super viewDidLayoutSubviews];
184
+ if (!_isTransitioning && self.isActiveAndVisible) {
185
+ // Flag that we are tracking position from layout
186
+ _isTrackingPositionFromLayout = YES;
187
+
188
+ // If another controller is presented on top, treat position changes as transitioning
189
+ // This prevents incorrect position notifications when overlays adjust our size
190
+ [self emitChangePositionDelegateWithPosition:self.currentPosition
191
+ transitioning:_layoutTransitioning || !self.isTopmostPresentedController];
192
+
193
+ // On IOS 26, this is called twice when we have a ScrollView
194
+ // Schedule flag reset after animation to avoid race condition
195
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
196
+ // Reset layout transitioning after sending notification
197
+ self->_layoutTransitioning = NO;
198
+ });
199
+ }
200
+ }
201
+
202
+ #pragma mark - Gesture Handling
203
+
204
+ /**
205
+ * Finds the TrueSheetContentView in the hierarchy.
206
+ *
207
+ * @param view The presentedView to start searching from
208
+ * @return The TrueSheetContentView found, or nil
209
+ */
210
+ - (TrueSheetContentView *)findContentView:(UIView *)view {
211
+ // Check if this view itself is TrueSheetContentView
212
+ if ([view isKindOfClass:[TrueSheetContentView class]]) {
213
+ return (TrueSheetContentView *)view;
214
+ }
215
+
216
+ // Recursively search all subviews
217
+ for (UIView *subview in view.subviews) {
218
+ TrueSheetContentView *found = [self findContentView:subview];
219
+ if (found) {
220
+ return found;
221
+ }
222
+ }
223
+
224
+ return nil;
225
+ }
226
+
227
+ - (void)setupGestureRecognizer {
228
+ UIView *presentedView = self.presentedView;
229
+ if (!presentedView)
230
+ return;
231
+
232
+ // Attach to presented view's pan gestures (sheet's own drag gesture from UIKit)
233
+ [GestureUtil attachPanGestureHandler:presentedView target:self selector:@selector(handlePanGesture:)];
234
+
235
+ // Find and attach to the first ScrollView's pan gesture in the view hierarchy
236
+ // This handles cases where the sheet content includes a ScrollView
237
+ TrueSheetContentView *contentView = [self findContentView:presentedView];
238
+ if (contentView) {
239
+ RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView];
240
+ if (scrollViewComponent) {
241
+ // Access the internal UIScrollView via the scrollView property
242
+ UIScrollView *scrollView = scrollViewComponent.scrollView;
243
+ if (scrollView) {
244
+ [GestureUtil attachPanGestureHandler:scrollView target:self selector:@selector(handlePanGesture:)];
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ - (void)handlePanGesture:(UIPanGestureRecognizer *)gesture {
251
+ NSInteger index = [self currentDetentIndex];
252
+
253
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidDrag:index:position:)]) {
254
+ [self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition];
255
+ }
256
+
257
+ switch (gesture.state) {
258
+ case UIGestureRecognizerStateBegan:
259
+ _isDragging = YES;
260
+ break;
261
+ case UIGestureRecognizerStateChanged:
262
+ if (!_isTrackingPositionFromLayout)
263
+ [self emitChangePositionDelegateWithPosition:self.currentPosition transitioning:NO];
264
+ break;
265
+ case UIGestureRecognizerStateEnded:
266
+ case UIGestureRecognizerStateCancelled:
267
+ _isDragging = NO;
268
+ break;
269
+
270
+ default:
271
+ break;
272
+ }
273
+ }
274
+
275
+ #pragma mark - Position Tracking
276
+
277
+ /**
278
+ * Emits position change to the delegate if the position has changed.
279
+ * @param transitioning Whether the position change is part of a transition animation
280
+ */
281
+ - (void)emitChangePositionDelegateWithPosition:(CGFloat)position transitioning:(BOOL)transitioning {
282
+ if (_lastPosition != position) {
283
+ _lastPosition = position;
284
+
285
+ // Emit position change delegate
286
+ NSInteger index = [self currentDetentIndex];
287
+
288
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:transitioning:)]) {
289
+ [self.delegate viewControllerDidChangePosition:index position:position transitioning:transitioning];
290
+ }
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Sets up position tracking during view controller transitions using a fake view.
296
+ *
297
+ * This approach uses a hidden "fake" view added to the container that animates
298
+ * alongside the actual presented view. By observing the presentation layer of this
299
+ * fake view's frame, we can track smooth position changes during the native transition
300
+ * animation without manually animating in JavaScript.
301
+ *
302
+ * The display link fires at screen refresh rate, allowing us to emit position updates
303
+ * that match the native animation curve, providing smooth synchronized updates to JS.
304
+ */
305
+ - (void)setupTransitionPositionTracking {
306
+ if (self.transitionCoordinator != nil) {
307
+ _isTransitioning = YES;
308
+
309
+ // Get the container view to add our fake transition view
310
+ UIView *containerView = self.sheetPresentationController.containerView;
311
+ UIView *presentedView = self.presentedView;
312
+
313
+ if (!containerView || !presentedView)
314
+ return;
315
+
316
+ CGRect frame = presentedView.frame;
317
+
318
+ // Determine if presenting or dismissing to set correct start position
319
+ BOOL isPresenting = self.isBeingPresented;
320
+
321
+ // Set initial position based on transition type:
322
+ // - Presenting: Start from bottom (containerHeight) and animate up to detent
323
+ // - Dismissing: Start from current position and animate down to bottom
324
+ if (isPresenting) {
325
+ frame.origin.y = self.containerHeight;
326
+ } else {
327
+ frame.origin.y = presentedView.frame.origin.y;
328
+ }
329
+
330
+ // Set fake view's initial frame before transition starts
331
+ _fakeTransitionView.frame = frame;
332
+
333
+ auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
334
+ // Add fake view to container so it participates in the transition
335
+ [[context containerView] addSubview:self->_fakeTransitionView];
336
+
337
+ // Animate fake view to the presented view's target position
338
+ // UIKit will animate this with the same timing curve as the sheet
339
+ CGRect finalFrame = presentedView.frame;
340
+ finalFrame.origin.y = presentedView.frame.origin.y;
341
+ self->_fakeTransitionView.frame = finalFrame;
342
+
343
+ // Set our last transition position so we can check if
344
+ // fake view's presentation layer has changed
345
+ // This value will not change if the sheet is being repositioned
346
+ // during a transition after a drag
347
+ self->_lastTransitionPosition = finalFrame.origin.y;
348
+
349
+ // Start display link to track position changes at screen refresh rate
350
+ // This fires at 60-120Hz and reads from the presentation layer
351
+ self->_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(trackTransitionPosition:)];
352
+ [self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
353
+ };
354
+
355
+ [self.transitionCoordinator
356
+ animateAlongsideTransition:animation
357
+ completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
358
+ // Clean up display link
359
+ [self->_displayLink invalidate];
360
+ self->_displayLink = nil;
361
+
362
+ // Remove fake view from hierarchy
363
+ [self->_fakeTransitionView removeFromSuperview];
364
+ self->_isTransitioning = NO;
365
+ }];
366
+ }
367
+ }
368
+
369
+ - (void)trackTransitionPosition:(CADisplayLink *)displayLink {
370
+ UIView *presentedView = self.presentedView;
371
+
372
+ if (_isDragging || !_fakeTransitionView || !presentedView) {
373
+ return;
374
+ }
375
+
376
+ // Get the presentation layer which contains the in-flight animated values
377
+ // Unlike the model layer (which has the final/target value), the presentation
378
+ // layer reflects the current state during animation
379
+ CALayer *presentationLayer = _fakeTransitionView.layer.presentationLayer;
380
+
381
+ if (presentationLayer) {
382
+ BOOL transitioning = NO;
383
+ CGFloat position = presentationLayer.frame.origin.y;
384
+
385
+ // Our last transition position is nearly the same as fake view's layer position
386
+ // Sheet must've been repositioning after dragging at lowest detent
387
+ if (fabs(_lastTransitionPosition - position) < FLT_EPSILON) {
388
+ // Let's just flag it as transitioning to let JS manually animate
389
+ transitioning = YES;
390
+
391
+ // Use the target presented view's frame position to animate
392
+ position = presentedView.frame.origin.y;
393
+ } else {
394
+ // We are actually getting changes to our fake view's layer position
395
+ // Just update our last transition position
396
+ _lastTransitionPosition = position;
397
+ }
398
+
399
+ // Emit the current animated Y position to JS
400
+ // This provides smooth position updates that match the native animation curve
401
+ [self emitChangePositionDelegateWithPosition:position transitioning:transitioning];
402
+ }
403
+ }
404
+
405
+ #pragma mark - Sheet Configuration (iOS 15+)
406
+
407
+ - (void)setupSheetDetents {
408
+ UISheetPresentationController *sheet = self.sheetPresentationController;
409
+ if (!sheet)
410
+ return;
411
+
412
+ NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
413
+
414
+ // Subtract bottomInset from content height to account for safe area
415
+ // This prevents iOS from adding extra bottom insets automatically
416
+ CGFloat totalHeight = [self.contentHeight floatValue] - _bottomInset;
417
+
418
+ for (NSInteger index = 0; index < self.detents.count; index++) {
419
+ id detent = self.detents[index];
420
+ UISheetPresentationControllerDetent *sheetDetent = [self detentForValue:detent
421
+ withHeight:totalHeight
422
+ atIndex:index];
423
+ [detents addObject:sheetDetent];
424
+ }
425
+
426
+ sheet.detents = detents;
427
+
428
+ // Setup dimmed background
429
+ if (self.dimmed && [self.dimmedIndex integerValue] == 0) {
430
+ sheet.largestUndimmedDetentIdentifier = nil;
431
+ } else {
432
+ sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
433
+
434
+ if (@available(iOS 16.0, *)) {
435
+ if (self.dimmed && self.dimmedIndex) {
436
+ NSInteger dimmedIdx = [self.dimmedIndex integerValue];
437
+ if (dimmedIdx > 0 && dimmedIdx - 1 < sheet.detents.count) {
438
+ sheet.largestUndimmedDetentIdentifier = sheet.detents[dimmedIdx - 1].identifier;
439
+ } else if (sheet.detents.lastObject) {
440
+ sheet.largestUndimmedDetentIdentifier = sheet.detents.lastObject.identifier;
441
+ }
442
+ } else if (sheet.detents.lastObject) {
443
+ sheet.largestUndimmedDetentIdentifier = sheet.detents.lastObject.identifier;
444
+ }
445
+ }
446
+ }
447
+ }
448
+
449
+ - (UISheetPresentationControllerDetent *)detentForValue:(id)detent withHeight:(CGFloat)height atIndex:(NSInteger)index {
450
+ if (![detent isKindOfClass:[NSNumber class]]) {
451
+ return [UISheetPresentationControllerDetent mediumDetent];
452
+ }
453
+
454
+ CGFloat value = [detent floatValue];
455
+
456
+ // -1 represents "auto"
457
+ if (value == -1) {
458
+ if (@available(iOS 16.0, *)) {
459
+ NSString *detentId = @"custom-auto";
460
+ return [UISheetPresentationControllerDetent
461
+ customDetentWithIdentifier:detentId
462
+ resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
463
+ CGFloat maxDetentValue = context.maximumDetentValue;
464
+ CGFloat maxValue =
465
+ self.maxHeight ? MIN(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
466
+ CGFloat resolvedValue = MIN(height, maxValue);
467
+ return resolvedValue;
468
+ }];
469
+ } else {
470
+ return [UISheetPresentationControllerDetent mediumDetent];
471
+ }
472
+ }
473
+
474
+ // Handle fraction (0-1)
475
+ // Fraction should only be > 0 and <= 1
476
+ if (value <= 0 || value > 1) {
477
+ RCTLogError(@"TrueSheet: detent fraction (%f) must be between 0 and 1", value);
478
+ return [UISheetPresentationControllerDetent mediumDetent];
479
+ }
480
+
481
+ NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
482
+
483
+ if (@available(iOS 16.0, *)) {
484
+ return [UISheetPresentationControllerDetent
485
+ customDetentWithIdentifier:detentId
486
+ resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
487
+ CGFloat maxDetentValue = context.maximumDetentValue;
488
+ CGFloat maxValue =
489
+ self.maxHeight ? MIN(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
490
+ CGFloat resolvedValue = MIN(value * maxDetentValue, maxValue);
491
+ return resolvedValue;
492
+ }];
493
+ } else {
494
+ return [UISheetPresentationControllerDetent mediumDetent];
495
+ }
496
+ }
497
+
498
+ - (UISheetPresentationControllerDetentIdentifier)detentIdentifierForIndex:(NSInteger)index {
499
+ UISheetPresentationController *sheet = self.sheetPresentationController;
500
+ if (!sheet)
501
+ return UISheetPresentationControllerDetentIdentifierMedium;
502
+
503
+ UISheetPresentationControllerDetentIdentifier identifier = UISheetPresentationControllerDetentIdentifierMedium;
504
+ if (index < sheet.detents.count) {
505
+ UISheetPresentationControllerDetent *detent = sheet.detents[index];
506
+ if (@available(iOS 16.0, *)) {
507
+ identifier = detent.identifier;
508
+ } else {
509
+ if (detent == [UISheetPresentationControllerDetent largeDetent]) {
510
+ identifier = UISheetPresentationControllerDetentIdentifierLarge;
511
+ }
512
+ }
513
+ }
514
+
515
+ return identifier;
516
+ }
517
+
518
+ - (void)applyActiveDetent {
519
+ UISheetPresentationController *sheet = self.sheetPresentationController;
520
+ if (!sheet)
521
+ return;
522
+
523
+ // Validate and clamp activeDetentIndex to detents bounds
524
+ NSInteger detentCount = _detents.count;
525
+ if (detentCount == 0) {
526
+ return;
527
+ }
528
+
529
+ // Clamp index to valid range
530
+ NSInteger clampedIndex = _activeDetentIndex;
531
+ if (clampedIndex < 0) {
532
+ clampedIndex = 0;
533
+ } else if (clampedIndex >= detentCount) {
534
+ clampedIndex = detentCount - 1;
535
+ }
536
+
537
+ // Update the stored index if it was clamped
538
+ if (clampedIndex != _activeDetentIndex) {
539
+ _activeDetentIndex = clampedIndex;
540
+ }
541
+
542
+ UISheetPresentationControllerDetentIdentifier identifier = [self detentIdentifierForIndex:clampedIndex];
543
+ if (identifier) {
544
+ sheet.selectedDetentIdentifier = identifier;
545
+ }
546
+ }
547
+
548
+ - (void)setupActiveDetentWithIndex:(NSInteger)index {
549
+ _activeDetentIndex = index;
550
+ [self applyActiveDetent];
551
+ }
552
+
553
+ - (NSInteger)currentDetentIndex {
554
+ UISheetPresentationController *sheet = self.sheetPresentationController;
555
+ if (!sheet)
556
+ return -1;
557
+
558
+ UISheetPresentationControllerDetentIdentifier selectedIdentifier = sheet.selectedDetentIdentifier;
559
+ if (!selectedIdentifier)
560
+ return -1;
561
+
562
+ // Find the index by matching the identifier in the detents array
563
+ NSArray<UISheetPresentationControllerDetent *> *detents = sheet.detents;
564
+ for (NSInteger i = 0; i < detents.count; i++) {
565
+ if (@available(iOS 16.0, *)) {
566
+ if ([detents[i].identifier isEqualToString:selectedIdentifier]) {
567
+ return i;
568
+ }
569
+ } else {
570
+ // For iOS 15, we only support system detents (medium/large)
571
+ // Return the index based on the selected identifier
572
+ if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierMedium]) {
573
+ return 0;
574
+ } else if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierLarge]) {
575
+ return detents.count - 1;
576
+ }
577
+ }
578
+ }
579
+
580
+ return -1;
581
+ }
582
+
583
+ - (CGFloat)currentPosition {
584
+ UIView *presentedView = self.presentedView;
585
+ if (!presentedView)
586
+ return 0.0;
587
+
588
+ return presentedView.frame.origin.y;
589
+ }
590
+
591
+ - (CGFloat)currentHeight {
592
+ return self.containerHeight - self.currentPosition - _bottomInset;
593
+ }
594
+
595
+ - (CGFloat)containerHeight {
596
+ UIView *containerView = self.sheetPresentationController.containerView;
597
+ if (!containerView)
598
+ return 0.0;
599
+
600
+ return containerView.frame.size.height;
601
+ }
602
+
603
+ - (void)setupSheetProps {
604
+ UISheetPresentationController *sheet = self.sheetPresentationController;
605
+ if (!sheet) {
606
+ return;
607
+ }
608
+
609
+ sheet.delegate = self;
610
+ sheet.prefersEdgeAttachedInCompactHeight = YES;
611
+ sheet.prefersGrabberVisible = self.grabber;
612
+ // Only set preferredCornerRadius if explicitly provided, otherwise use system default
613
+ if (self.cornerRadius) {
614
+ sheet.preferredCornerRadius = [self.cornerRadius floatValue];
615
+ }
616
+
617
+ // Setup blur effect if blurTint is provided
618
+ if (self.blurTint && self.blurTint.length > 0) {
619
+ UIBlurEffectStyle style = UIBlurEffectStyleLight;
620
+
621
+ if ([self.blurTint isEqualToString:@"dark"]) {
622
+ style = UIBlurEffectStyleDark;
623
+ } else if ([self.blurTint isEqualToString:@"light"]) {
624
+ style = UIBlurEffectStyleLight;
625
+ } else if ([self.blurTint isEqualToString:@"extraLight"]) {
626
+ style = UIBlurEffectStyleExtraLight;
627
+ } else if ([self.blurTint isEqualToString:@"regular"]) {
628
+ style = UIBlurEffectStyleRegular;
629
+ } else if ([self.blurTint isEqualToString:@"prominent"]) {
630
+ style = UIBlurEffectStyleProminent;
631
+ } else if ([self.blurTint isEqualToString:@"systemThinMaterial"]) {
632
+ style = UIBlurEffectStyleSystemThinMaterial;
633
+ } else if ([self.blurTint isEqualToString:@"systemMaterial"]) {
634
+ style = UIBlurEffectStyleSystemMaterial;
635
+ } else if ([self.blurTint isEqualToString:@"systemThickMaterial"]) {
636
+ style = UIBlurEffectStyleSystemThickMaterial;
637
+ } else if ([self.blurTint isEqualToString:@"systemChromeMaterial"]) {
638
+ style = UIBlurEffectStyleSystemChromeMaterial;
639
+ } else if ([self.blurTint isEqualToString:@"systemUltraThinMaterial"]) {
640
+ style = UIBlurEffectStyleSystemUltraThinMaterial;
641
+ }
642
+
643
+ _backgroundView.effect = [UIBlurEffect effectWithStyle:style];
644
+ _backgroundView.backgroundColor = nil;
645
+ } else {
646
+ // No blur effect, use solid background color
647
+ _backgroundView.effect = nil;
648
+ _backgroundView.backgroundColor = self.backgroundColor;
649
+ }
650
+ }
651
+
652
+ #pragma mark - UISheetPresentationControllerDelegate
653
+
654
+ - (void)sheetPresentationControllerDidChangeSelectedDetentIdentifier:
655
+ (UISheetPresentationController *)sheetPresentationController {
656
+ NSInteger index = [self currentDetentIndex];
657
+ if (index >= 0 && [self.delegate respondsToSelector:@selector(viewControllerDidChangeDetent:position:)]) {
658
+ [self.delegate viewControllerDidChangeDetent:index position:self.currentPosition];
659
+ }
660
+ }
661
+
662
+ #pragma mark - RNSDismissibleModalProtocol
663
+
664
+ #if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
665
+ - (BOOL)isDismissible {
666
+ // Return NO to prevent react-native-screens from dismissing this sheet
667
+ // when presenting a React Navigation modal
668
+ return NO;
669
+ }
670
+ #endif
671
+
672
+ @end
@@ -0,0 +1,28 @@
1
+ //
2
+ // Created by Jovanni Lo (@lodev09)
3
+ // Copyright (c) 2024-present. All rights reserved.
4
+ //
5
+ // This source code is licensed under the MIT license found in the
6
+ // LICENSE file in the root directory of this source tree.
7
+ //
8
+
9
+ #ifdef RCT_NEW_ARCH_ENABLED
10
+
11
+ #import <Foundation/Foundation.h>
12
+ #import <react/renderer/components/TrueSheetSpec/EventEmitters.h>
13
+
14
+ using namespace facebook::react;
15
+
16
+ NS_ASSUME_NONNULL_BEGIN
17
+
18
+ @interface OnDetentChangeEvent : NSObject
19
+
20
+ + (void)emit:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter
21
+ index:(NSInteger)index
22
+ position:(CGFloat)position;
23
+
24
+ @end
25
+
26
+ NS_ASSUME_NONNULL_END
27
+
28
+ #endif // RCT_NEW_ARCH_ENABLED