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

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 -299
  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 +433 -0
  39. package/ios/TrueSheetViewController.h +53 -0
  40. package/ios/TrueSheetViewController.mm +649 -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 -400
  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 -461
  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,649 @@
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
+ return self;
63
+ }
64
+
65
+ - (void)dealloc {
66
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
67
+ }
68
+
69
+ #pragma mark - Presentation State
70
+
71
+ - (BOOL)isTopmostPresentedController {
72
+ // Check if we're in the window hierarchy and visible
73
+ if (!self.isViewLoaded || self.view.window == nil) {
74
+ return NO;
75
+ }
76
+
77
+ // Check if another controller is presented on top of this sheet
78
+ return self.presentedViewController == nil;
79
+ }
80
+
81
+ - (BOOL)isActiveAndVisible {
82
+ return self.isViewLoaded && self.view.window != nil && !self.isBeingDismissed;
83
+ }
84
+
85
+ - (UIView *)presentedView {
86
+ return self.sheetPresentationController.presentedView;
87
+ }
88
+
89
+ - (void)viewDidLoad {
90
+ [super viewDidLoad];
91
+
92
+ self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
93
+ _backgroundView.frame = self.view.bounds;
94
+ [self.view insertSubview:self->_backgroundView atIndex:0];
95
+ }
96
+
97
+ - (void)viewWillAppear:(BOOL)animated {
98
+ [super viewWillAppear:animated];
99
+
100
+ // Only trigger willPresent on the initial presentation, not on repositioning
101
+ if (!_isPresented) {
102
+ if ([self.delegate respondsToSelector:@selector(viewControllerWillPresent)]) {
103
+ [self.delegate viewControllerWillPresent];
104
+ }
105
+
106
+ // Setup transition position tracking
107
+ [self setupTransitionPositionTracking];
108
+ }
109
+ }
110
+
111
+ - (void)viewDidAppear:(BOOL)animated {
112
+ [super viewDidAppear:animated];
113
+
114
+ // Only trigger didPresent on the initial presentation, not on repositioning
115
+ if (!_isPresented) {
116
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidPresent)]) {
117
+ [self.delegate viewControllerDidPresent];
118
+ }
119
+
120
+ // Setup gesture recognizer after view appears and React content is mounted
121
+ [self setupGestureRecognizer];
122
+
123
+ _isPresented = YES;
124
+ }
125
+ }
126
+
127
+ - (void)viewWillDisappear:(BOOL)animated {
128
+ [super viewWillDisappear:animated];
129
+
130
+ if ([self.delegate respondsToSelector:@selector(viewControllerWillDismiss)]) {
131
+ [self.delegate viewControllerWillDismiss];
132
+ }
133
+
134
+ [self setupTransitionPositionTracking];
135
+ _isTrackingPositionFromLayout = NO;
136
+ }
137
+
138
+ - (void)viewDidDisappear:(BOOL)animated {
139
+ [super viewDidDisappear:animated];
140
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidDismiss)]) {
141
+ [self.delegate viewControllerDidDismiss];
142
+ }
143
+
144
+ _isTrackingPositionFromLayout = NO;
145
+ _isPresented = NO;
146
+ _activeDetentIndex = -1;
147
+ }
148
+
149
+ - (void)viewWillTransitionToSize:(CGSize)size
150
+ withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
151
+ [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
152
+
153
+ // Handle rotation/size change
154
+ [coordinator
155
+ animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
156
+ // Animation block - updates happen here
157
+ }
158
+ completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
159
+ // After rotation completes
160
+ [self setupSheetDetents];
161
+
162
+ // Notify delegate of size changes
163
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeSize:)]) {
164
+ [self.delegate viewControllerDidChangeSize:self.view.frame.size];
165
+ }
166
+ }];
167
+ }
168
+
169
+ - (void)viewDidLayoutSubviews {
170
+ [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;
185
+ });
186
+ }
187
+ }
188
+
189
+ #pragma mark - Gesture Handling
190
+
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
+ - (TrueSheetContentView *)findContentView:(UIView *)view {
198
+ // Check if this view itself is TrueSheetContentView
199
+ if ([view isKindOfClass:[TrueSheetContentView class]]) {
200
+ return (TrueSheetContentView *)view;
201
+ }
202
+
203
+ // Recursively search all subviews
204
+ for (UIView *subview in view.subviews) {
205
+ TrueSheetContentView *found = [self findContentView:subview];
206
+ if (found) {
207
+ return found;
208
+ }
209
+ }
210
+
211
+ return nil;
212
+ }
213
+
214
+ - (void)setupGestureRecognizer {
215
+ UIView *presentedView = self.presentedView;
216
+ if (!presentedView)
217
+ return;
218
+
219
+ // Attach to presented view's pan gestures (sheet's own drag gesture from UIKit)
220
+ [GestureUtil attachPanGestureHandler:presentedView target:self selector:@selector(handlePanGesture:)];
221
+
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
+ TrueSheetContentView *contentView = [self findContentView:presentedView];
225
+ if (contentView) {
226
+ 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
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ - (void)handlePanGesture:(UIPanGestureRecognizer *)gesture {
238
+ NSInteger index = [self currentDetentIndex];
239
+
240
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidDrag:index:position:)]) {
241
+ [self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition];
242
+ }
243
+
244
+ switch (gesture.state) {
245
+ case UIGestureRecognizerStateBegan:
246
+ _isDragging = YES;
247
+ break;
248
+ case UIGestureRecognizerStateChanged:
249
+ if (!_isTrackingPositionFromLayout)
250
+ [self emitChangePositionDelegateWithPosition:self.currentPosition transitioning:NO];
251
+ break;
252
+ case UIGestureRecognizerStateEnded:
253
+ case UIGestureRecognizerStateCancelled:
254
+ _isDragging = NO;
255
+ break;
256
+
257
+ default:
258
+ break;
259
+ }
260
+ }
261
+
262
+ #pragma mark - Position Tracking
263
+
264
+ /**
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
267
+ */
268
+ - (void)emitChangePositionDelegateWithPosition:(CGFloat)position transitioning:(BOOL)transitioning {
269
+ if (_lastPosition != position) {
270
+ _lastPosition = position;
271
+
272
+ // Emit position change delegate
273
+ NSInteger index = [self currentDetentIndex];
274
+
275
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:transitioning:)]) {
276
+ [self.delegate viewControllerDidChangePosition:index position:position transitioning:transitioning];
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
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.
291
+ */
292
+ - (void)setupTransitionPositionTracking {
293
+ if (self.transitionCoordinator != nil) {
294
+ _isTransitioning = YES;
295
+
296
+ // Get the container view to add our fake transition view
297
+ UIView *containerView = self.sheetPresentationController.containerView;
298
+ UIView *presentedView = self.presentedView;
299
+
300
+ if (!containerView || !presentedView)
301
+ return;
302
+
303
+ CGRect frame = presentedView.frame;
304
+
305
+ // Determine if presenting or dismissing to set correct start position
306
+ BOOL isPresenting = self.isBeingPresented;
307
+
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
+ }
316
+
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
+ };
341
+
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;
348
+
349
+ // Remove fake view from hierarchy
350
+ [self->_fakeTransitionView removeFromSuperview];
351
+ self->_isTransitioning = NO;
352
+ }];
353
+ }
354
+ }
355
+
356
+ - (void)trackTransitionPosition:(CADisplayLink *)displayLink {
357
+ UIView *presentedView = self.presentedView;
358
+
359
+ if (_isDragging || !_fakeTransitionView || !presentedView) {
360
+ return;
361
+ }
362
+
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;
367
+
368
+ if (presentationLayer) {
369
+ BOOL transitioning = NO;
370
+ CGFloat position = presentationLayer.frame.origin.y;
371
+
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;
377
+
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;
384
+ }
385
+
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];
389
+ }
390
+ }
391
+
392
+ #pragma mark - Sheet Configuration (iOS 15+)
393
+
394
+ - (void)setupSheetDetents {
395
+ UISheetPresentationController *sheet = self.sheetPresentationController;
396
+ if (!sheet)
397
+ return;
398
+
399
+ NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
400
+
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;
404
+
405
+ for (NSInteger index = 0; index < self.detents.count; index++) {
406
+ id detent = self.detents[index];
407
+ UISheetPresentationControllerDetent *sheetDetent = [self detentForValue:detent
408
+ withHeight:totalHeight
409
+ atIndex:index];
410
+ [detents addObject:sheetDetent];
411
+ }
412
+
413
+ sheet.detents = detents;
414
+
415
+ // Setup dimmed background
416
+ if (self.dimmed && [self.dimmedIndex integerValue] == 0) {
417
+ sheet.largestUndimmedDetentIdentifier = nil;
418
+ } else {
419
+ sheet.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
420
+
421
+ if (@available(iOS 16.0, *)) {
422
+ if (self.dimmed && self.dimmedIndex) {
423
+ NSInteger dimmedIdx = [self.dimmedIndex integerValue];
424
+ if (dimmedIdx > 0 && dimmedIdx - 1 < sheet.detents.count) {
425
+ sheet.largestUndimmedDetentIdentifier = sheet.detents[dimmedIdx - 1].identifier;
426
+ } else if (sheet.detents.lastObject) {
427
+ sheet.largestUndimmedDetentIdentifier = sheet.detents.lastObject.identifier;
428
+ }
429
+ } else if (sheet.detents.lastObject) {
430
+ sheet.largestUndimmedDetentIdentifier = sheet.detents.lastObject.identifier;
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ - (UISheetPresentationControllerDetent *)detentForValue:(id)detent withHeight:(CGFloat)height atIndex:(NSInteger)index {
437
+ if (![detent isKindOfClass:[NSNumber class]]) {
438
+ return [UISheetPresentationControllerDetent mediumDetent];
439
+ }
440
+
441
+ CGFloat value = [detent floatValue];
442
+
443
+ // -1 represents "auto"
444
+ if (value == -1) {
445
+ 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
+ }];
456
+ } else {
457
+ return [UISheetPresentationControllerDetent mediumDetent];
458
+ }
459
+ }
460
+
461
+ // Handle fraction (0-1)
462
+ // Fraction should only be > 0 and <= 1
463
+ if (value <= 0 || value > 1) {
464
+ RCTLogError(@"TrueSheet: detent fraction (%f) must be between 0 and 1", value);
465
+ return [UISheetPresentationControllerDetent mediumDetent];
466
+ }
467
+
468
+ NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
469
+
470
+ 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
+ }];
480
+ } else {
481
+ return [UISheetPresentationControllerDetent mediumDetent];
482
+ }
483
+ }
484
+
485
+ - (UISheetPresentationControllerDetentIdentifier)detentIdentifierForIndex:(NSInteger)index {
486
+ UISheetPresentationController *sheet = self.sheetPresentationController;
487
+ if (!sheet)
488
+ return UISheetPresentationControllerDetentIdentifierMedium;
489
+
490
+ UISheetPresentationControllerDetentIdentifier identifier = UISheetPresentationControllerDetentIdentifierMedium;
491
+ if (index < sheet.detents.count) {
492
+ UISheetPresentationControllerDetent *detent = sheet.detents[index];
493
+ if (@available(iOS 16.0, *)) {
494
+ identifier = detent.identifier;
495
+ } else {
496
+ if (detent == [UISheetPresentationControllerDetent largeDetent]) {
497
+ identifier = UISheetPresentationControllerDetentIdentifierLarge;
498
+ }
499
+ }
500
+ }
501
+
502
+ return identifier;
503
+ }
504
+
505
+ - (void)applyActiveDetent {
506
+ UISheetPresentationController *sheet = self.sheetPresentationController;
507
+ if (!sheet)
508
+ return;
509
+
510
+ // Validate and clamp activeDetentIndex to detents bounds
511
+ NSInteger detentCount = _detents.count;
512
+ if (detentCount == 0) {
513
+ return;
514
+ }
515
+
516
+ // Clamp index to valid range
517
+ NSInteger clampedIndex = _activeDetentIndex;
518
+ if (clampedIndex < 0) {
519
+ clampedIndex = 0;
520
+ } else if (clampedIndex >= detentCount) {
521
+ clampedIndex = detentCount - 1;
522
+ }
523
+
524
+ // Update the stored index if it was clamped
525
+ if (clampedIndex != _activeDetentIndex) {
526
+ _activeDetentIndex = clampedIndex;
527
+ }
528
+
529
+ UISheetPresentationControllerDetentIdentifier identifier = [self detentIdentifierForIndex:clampedIndex];
530
+ if (identifier) {
531
+ sheet.selectedDetentIdentifier = identifier;
532
+ }
533
+ }
534
+
535
+ - (void)setupActiveDetentWithIndex:(NSInteger)index {
536
+ _activeDetentIndex = index;
537
+ [self applyActiveDetent];
538
+ }
539
+
540
+ - (NSInteger)currentDetentIndex {
541
+ UISheetPresentationController *sheet = self.sheetPresentationController;
542
+ if (!sheet)
543
+ return -1;
544
+
545
+ UISheetPresentationControllerDetentIdentifier selectedIdentifier = sheet.selectedDetentIdentifier;
546
+ if (!selectedIdentifier)
547
+ return -1;
548
+
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;
563
+ }
564
+ }
565
+ }
566
+
567
+ return -1;
568
+ }
569
+
570
+ - (CGFloat)currentPosition {
571
+ UIView *presentedView = self.presentedView;
572
+ if (!presentedView)
573
+ return 0.0;
574
+
575
+ return presentedView.frame.origin.y;
576
+ }
577
+
578
+ - (CGFloat)currentHeight {
579
+ return self.containerHeight - self.currentPosition - _bottomInset;
580
+ }
581
+
582
+ - (CGFloat)containerHeight {
583
+ UIView *containerView = self.sheetPresentationController.containerView;
584
+ if (!containerView)
585
+ return 0.0;
586
+
587
+ return containerView.frame.size.height;
588
+ }
589
+
590
+ - (void)setupSheetProps {
591
+ UISheetPresentationController *sheet = self.sheetPresentationController;
592
+ if (!sheet) {
593
+ return;
594
+ }
595
+
596
+ sheet.delegate = self;
597
+ sheet.prefersEdgeAttachedInCompactHeight = YES;
598
+ sheet.prefersGrabberVisible = self.grabber;
599
+ // Only set preferredCornerRadius if explicitly provided, otherwise use system default
600
+ if (self.cornerRadius) {
601
+ 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
+ } else {
633
+ // No blur effect, use solid background color
634
+ _backgroundView.effect = nil;
635
+ _backgroundView.backgroundColor = self.backgroundColor;
636
+ }
637
+ }
638
+
639
+ #pragma mark - UISheetPresentationControllerDelegate
640
+
641
+ - (void)sheetPresentationControllerDidChangeSelectedDetentIdentifier:
642
+ (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];
646
+ }
647
+ }
648
+
649
+ @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
@@ -0,0 +1,30 @@
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 "OnDetentChangeEvent.h"
12
+
13
+ @implementation OnDetentChangeEvent
14
+
15
+ + (void)emit:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter
16
+ index:(NSInteger)index
17
+ position:(CGFloat)position {
18
+ if (!eventEmitter)
19
+ return;
20
+
21
+ auto emitter = std::static_pointer_cast<TrueSheetViewEventEmitter const>(eventEmitter);
22
+ TrueSheetViewEventEmitter::OnDetentChange event;
23
+ event.index = static_cast<int>(index);
24
+ event.position = static_cast<double>(position);
25
+ emitter->onDetentChange(event);
26
+ }
27
+
28
+ @end
29
+
30
+ #endif // RCT_NEW_ARCH_ENABLED