@lodev09/react-native-true-sheet 3.0.0-beta.9 → 3.0.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 (101) hide show
  1. package/README.md +16 -6
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +29 -33
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetModule.kt +3 -1
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +53 -43
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +390 -89
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +42 -4
  7. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +0 -5
  8. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogObserver.kt +67 -0
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetGrabberView.kt +70 -0
  10. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetDragEvents.kt +71 -0
  11. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetFocusEvents.kt +65 -0
  12. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +94 -0
  13. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetStateEvents.kt +56 -0
  14. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +37 -33
  15. package/android/src/main/res/anim/true_sheet_slide_in.xml +13 -0
  16. package/android/src/main/res/anim/true_sheet_slide_out.xml +13 -0
  17. package/android/src/main/res/values/styles.xml +13 -1
  18. package/common/cpp/react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.cpp +5 -3
  19. package/ios/TrueSheetContainerView.mm +4 -0
  20. package/ios/TrueSheetContentView.h +2 -1
  21. package/ios/TrueSheetContentView.mm +91 -11
  22. package/ios/TrueSheetView.mm +94 -41
  23. package/ios/TrueSheetViewController.h +22 -10
  24. package/ios/TrueSheetViewController.mm +360 -173
  25. package/ios/core/TrueSheetBlurView.h +26 -0
  26. package/ios/{utils/ConversionUtil.mm → core/TrueSheetBlurView.mm} +64 -3
  27. package/ios/core/TrueSheetGrabberView.h +42 -0
  28. package/ios/core/TrueSheetGrabberView.mm +107 -0
  29. package/ios/events/TrueSheetDragEvents.h +39 -0
  30. package/ios/events/TrueSheetDragEvents.mm +62 -0
  31. package/ios/events/{OnPositionChangeEvent.h → TrueSheetFocusEvents.h} +8 -5
  32. package/ios/events/TrueSheetFocusEvents.mm +49 -0
  33. package/ios/events/TrueSheetLifecycleEvents.h +40 -0
  34. package/ios/events/TrueSheetLifecycleEvents.mm +71 -0
  35. package/ios/events/TrueSheetStateEvents.h +35 -0
  36. package/ios/events/TrueSheetStateEvents.mm +49 -0
  37. package/ios/utils/GestureUtil.h +7 -0
  38. package/ios/utils/GestureUtil.mm +12 -0
  39. package/lib/module/TrueSheet.js +72 -12
  40. package/lib/module/TrueSheet.js.map +1 -1
  41. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +28 -5
  42. package/lib/module/index.js +0 -1
  43. package/lib/module/index.js.map +1 -1
  44. package/lib/module/reanimated/ReanimatedTrueSheet.js +13 -7
  45. package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -1
  46. package/lib/module/reanimated/ReanimatedTrueSheetProvider.js +4 -2
  47. package/lib/module/reanimated/ReanimatedTrueSheetProvider.js.map +1 -1
  48. package/lib/typescript/src/TrueSheet.d.ts +4 -0
  49. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  50. package/lib/typescript/src/TrueSheet.types.d.ts +105 -6
  51. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  52. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +25 -5
  53. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  54. package/lib/typescript/src/index.d.ts +0 -1
  55. package/lib/typescript/src/index.d.ts.map +1 -1
  56. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.d.ts.map +1 -1
  57. package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts +8 -2
  58. package/lib/typescript/src/reanimated/ReanimatedTrueSheetProvider.d.ts.map +1 -1
  59. package/package.json +8 -2
  60. package/src/TrueSheet.tsx +87 -10
  61. package/src/TrueSheet.types.ts +114 -6
  62. package/src/__mocks__/index.js +0 -5
  63. package/src/fabric/TrueSheetViewNativeComponent.ts +28 -5
  64. package/src/index.ts +0 -1
  65. package/src/reanimated/ReanimatedTrueSheet.tsx +12 -7
  66. package/src/reanimated/ReanimatedTrueSheetProvider.tsx +11 -3
  67. package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +0 -26
  68. package/android/src/main/java/com/lodev09/truesheet/events/DidDismissEvent.kt +0 -20
  69. package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +0 -26
  70. package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +0 -26
  71. package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +0 -26
  72. package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +0 -26
  73. package/android/src/main/java/com/lodev09/truesheet/events/MountEvent.kt +0 -20
  74. package/android/src/main/java/com/lodev09/truesheet/events/PositionChangeEvent.kt +0 -32
  75. package/android/src/main/java/com/lodev09/truesheet/events/WillDismissEvent.kt +0 -20
  76. package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +0 -26
  77. package/ios/events/OnDetentChangeEvent.h +0 -28
  78. package/ios/events/OnDetentChangeEvent.mm +0 -30
  79. package/ios/events/OnDidDismissEvent.h +0 -26
  80. package/ios/events/OnDidDismissEvent.mm +0 -25
  81. package/ios/events/OnDidPresentEvent.h +0 -28
  82. package/ios/events/OnDidPresentEvent.mm +0 -30
  83. package/ios/events/OnDragBeginEvent.h +0 -28
  84. package/ios/events/OnDragBeginEvent.mm +0 -30
  85. package/ios/events/OnDragChangeEvent.h +0 -28
  86. package/ios/events/OnDragChangeEvent.mm +0 -30
  87. package/ios/events/OnDragEndEvent.h +0 -28
  88. package/ios/events/OnDragEndEvent.mm +0 -30
  89. package/ios/events/OnMountEvent.h +0 -26
  90. package/ios/events/OnMountEvent.mm +0 -25
  91. package/ios/events/OnPositionChangeEvent.mm +0 -32
  92. package/ios/events/OnWillDismissEvent.h +0 -26
  93. package/ios/events/OnWillDismissEvent.mm +0 -25
  94. package/ios/events/OnWillPresentEvent.h +0 -28
  95. package/ios/events/OnWillPresentEvent.mm +0 -30
  96. package/ios/utils/ConversionUtil.h +0 -24
  97. package/lib/module/TrueSheetGrabber.js +0 -51
  98. package/lib/module/TrueSheetGrabber.js.map +0 -1
  99. package/lib/typescript/src/TrueSheetGrabber.d.ts +0 -39
  100. package/lib/typescript/src/TrueSheetGrabber.d.ts.map +0 -1
  101. package/src/TrueSheetGrabber.tsx +0 -82
@@ -8,7 +8,8 @@
8
8
 
9
9
  #import "TrueSheetViewController.h"
10
10
  #import "TrueSheetContentView.h"
11
- #import "utils/ConversionUtil.h"
11
+ #import "core/TrueSheetBlurView.h"
12
+ #import "core/TrueSheetGrabberView.h"
12
13
  #import "utils/GestureUtil.h"
13
14
  #import "utils/WindowUtil.h"
14
15
 
@@ -21,14 +22,20 @@
21
22
 
22
23
  @implementation TrueSheetViewController {
23
24
  CGFloat _lastPosition;
24
- CGFloat _lastTransitionPosition;
25
- BOOL _isTransitioning;
26
25
  BOOL _isDragging;
27
- BOOL _isTrackingPositionFromLayout;
26
+ NSInteger _pendingDetentIndex;
28
27
 
29
- // Hidden view used to track position during native transition animations
30
- UIView *_fakeTransitionView;
31
- CADisplayLink *_displayLink;
28
+ // Reference to parent TrueSheetViewController (if presented from another sheet)
29
+ __weak TrueSheetViewController *_parentSheetController;
30
+
31
+ // Blur effect view
32
+ TrueSheetBlurView *_blurView;
33
+
34
+ // Custom grabber view
35
+ TrueSheetGrabberView *_grabberView;
36
+
37
+ // Resolved detent positions (Y coordinate when sheet rests at each detent)
38
+ NSMutableArray<NSNumber *> *_resolvedDetentPositions;
32
39
  }
33
40
 
34
41
  #pragma mark - Initialization
@@ -39,32 +46,24 @@
39
46
  _contentHeight = @(0);
40
47
  _headerHeight = @(0);
41
48
  _grabber = YES;
49
+ _draggable = YES;
42
50
  _dimmed = YES;
43
51
  _dimmedDetentIndex = @(0);
44
52
  _pageSizing = YES;
45
53
  _lastPosition = 0;
46
- _lastTransitionPosition = 0;
47
- _isTransitioning = NO;
48
54
  _isDragging = NO;
49
- _isTrackingPositionFromLayout = NO;
50
- _layoutTransitioning = NO;
51
55
  _isPresented = NO;
52
56
  _activeDetentIndex = -1;
57
+ _pendingDetentIndex = -1;
53
58
 
54
- _fakeTransitionView = [[UIView alloc] init];
55
- _fakeTransitionView.hidden = YES;
56
- _fakeTransitionView.userInteractionEnabled = NO;
59
+ _blurInteraction = YES;
60
+ _resolvedDetentPositions = [NSMutableArray array];
57
61
  }
58
62
  return self;
59
63
  }
60
64
 
61
65
  - (void)dealloc {
62
66
  [[NSNotificationCenter defaultCenter] removeObserver:self];
63
-
64
- if (_displayLink) {
65
- [_displayLink invalidate];
66
- _displayLink = nil;
67
- }
68
67
  }
69
68
 
70
69
  #pragma mark - Computed Properties
@@ -77,7 +76,7 @@
77
76
  }
78
77
 
79
78
  - (BOOL)isActiveAndVisible {
80
- return self.isViewLoaded && self.view.window != nil && !self.isBeingDismissed;
79
+ return self.isViewLoaded && self.view.window != nil;
81
80
  }
82
81
 
83
82
  - (UIView *)presentedView {
@@ -89,21 +88,8 @@
89
88
  return presentedView ? presentedView.frame.origin.y : 0.0;
90
89
  }
91
90
 
92
- - (CGFloat)bottomInset {
93
- if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
94
- return 0;
95
- }
96
- UIWindow *window = [WindowUtil keyWindow];
97
- return window ? window.safeAreaInsets.bottom : 0;
98
- }
99
-
100
- - (CGFloat)currentHeight {
101
- return self.containerHeight - self.currentPosition - self.bottomInset;
102
- }
103
-
104
- - (CGFloat)containerHeight {
105
- UIView *sheetContainerView = self.sheetPresentationController.containerView;
106
- return sheetContainerView ? sheetContainerView.frame.size.height : 0.0;
91
+ - (CGFloat)screenHeight {
92
+ return UIScreen.mainScreen.bounds.size.height;
107
93
  }
108
94
 
109
95
  - (NSInteger)currentDetentIndex {
@@ -139,6 +125,11 @@
139
125
  - (void)viewDidLoad {
140
126
  [super viewDidLoad];
141
127
  self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
128
+
129
+ // Create custom grabber view (hidden by default, shown when grabberOptions is set)
130
+ _grabberView = [[TrueSheetGrabberView alloc] init];
131
+ _grabberView.hidden = YES;
132
+ [_grabberView addToView:self.view];
142
133
  }
143
134
 
144
135
  - (void)viewWillAppear:(BOOL)animated {
@@ -146,10 +137,25 @@
146
137
 
147
138
  // Only trigger on initial presentation, not repositioning
148
139
  if (!_isPresented) {
149
- if ([self.delegate respondsToSelector:@selector(viewControllerWillPresent)]) {
150
- [self.delegate viewControllerWillPresent];
140
+ // Capture parent sheet reference if presented from another TrueSheet
141
+ UIViewController *presenter = self.presentingViewController;
142
+ if ([presenter isKindOfClass:[TrueSheetViewController class]]) {
143
+ _parentSheetController = (TrueSheetViewController *)presenter;
144
+ // Notify parent that it is about to lose focus
145
+ if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerWillBlur)]) {
146
+ [_parentSheetController.delegate viewControllerWillBlur];
147
+ }
148
+ }
149
+
150
+ if ([self.delegate respondsToSelector:@selector(viewControllerWillPresentAtIndex:position:detent:)]) {
151
+ dispatch_async(dispatch_get_main_queue(), ^{
152
+ NSInteger index = [self currentDetentIndex];
153
+ CGFloat position = self.currentPosition;
154
+ CGFloat detent = [self detentValueForIndex:index];
155
+
156
+ [self.delegate viewControllerWillPresentAtIndex:index position:position detent:detent];
157
+ });
151
158
  }
152
- [self setupTransitionPositionTracking];
153
159
  }
154
160
  }
155
161
 
@@ -157,40 +163,69 @@
157
163
  [super viewDidAppear:animated];
158
164
 
159
165
  if (!_isPresented) {
160
- if ([self.delegate respondsToSelector:@selector(viewControllerDidPresent)]) {
161
- [self.delegate viewControllerDidPresent];
166
+ // Notify parent that it has lost focus (after the child sheet appeared)
167
+ if (_parentSheetController) {
168
+ if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidBlur)]) {
169
+ [_parentSheetController.delegate viewControllerDidBlur];
170
+ }
171
+ }
172
+
173
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidPresentAtIndex:position:detent:)]) {
174
+ dispatch_async(dispatch_get_main_queue(), ^{
175
+ NSInteger index = [self currentDetentIndex];
176
+ CGFloat detent = [self detentValueForIndex:index];
177
+ [self.delegate viewControllerDidPresentAtIndex:index position:self.currentPosition detent:detent];
178
+ });
162
179
  }
163
180
  [self setupGestureRecognizer];
164
181
  _isPresented = YES;
165
182
  }
166
183
  }
167
184
 
185
+ - (BOOL)isDismissing {
186
+ return self.presentingViewController == nil || self.isBeingDismissed;
187
+ }
188
+
168
189
  - (void)viewWillDisappear:(BOOL)animated {
169
190
  [super viewWillDisappear:animated];
170
191
 
171
- if (self.isBeingDismissed && [self.delegate respondsToSelector:@selector(viewControllerWillDismiss)]) {
172
- [self.delegate viewControllerWillDismiss];
173
- }
192
+ if (self.isDismissing) {
193
+ _isPresented = NO;
194
+ _activeDetentIndex = -1;
195
+
196
+ // Notify the parent sheet (if any) that it is about to regain focus
197
+ if (_parentSheetController) {
198
+ if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerWillFocus)]) {
199
+ [_parentSheetController.delegate viewControllerWillFocus];
200
+ }
201
+ }
174
202
 
175
- [self setupTransitionPositionTracking];
176
- _isTrackingPositionFromLayout = NO;
203
+ dispatch_async(dispatch_get_main_queue(), ^{
204
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO];
205
+ });
206
+
207
+ if ([self.delegate respondsToSelector:@selector(viewControllerWillDismiss)]) {
208
+ [self.delegate viewControllerWillDismiss];
209
+ }
210
+ }
177
211
  }
178
212
 
179
213
  - (void)viewDidDisappear:(BOOL)animated {
180
214
  [super viewDidDisappear:animated];
181
215
 
182
216
  // Only dispatch didDismiss when actually dismissing (not when another modal is presented on top)
183
- BOOL isActuallyDismissing = self.presentingViewController == nil || self.isBeingDismissed;
184
-
185
- if (isActuallyDismissing && [self.delegate respondsToSelector:@selector(viewControllerDidDismiss)]) {
186
- [self.delegate viewControllerDidDismiss];
187
- }
188
-
189
- _isTrackingPositionFromLayout = NO;
217
+ if (self.isDismissing) {
218
+ // Notify the parent sheet (if any) that it regained focus
219
+ if (_parentSheetController) {
220
+ if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidFocus)]) {
221
+ [_parentSheetController.delegate viewControllerDidFocus];
222
+ }
223
+ _parentSheetController = nil;
224
+ }
190
225
 
191
- if (isActuallyDismissing) {
192
- _isPresented = NO;
193
- _activeDetentIndex = -1;
226
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidDismiss)]) {
227
+ [self.delegate viewControllerDidDismiss];
228
+ }
194
229
  }
195
230
  }
196
231
 
@@ -201,16 +236,29 @@
201
236
  [self.delegate viewControllerDidChangeSize:self.view.frame.size];
202
237
  }
203
238
 
204
- if (!_isTransitioning && self.isActiveAndVisible) {
205
- _isTrackingPositionFromLayout = YES;
239
+ // Check if there's an active presented controller that has settled (not being presented/dismissed)
240
+ UIViewController *presented = self.presentedViewController;
241
+ BOOL hasPresentedController = presented != nil && !presented.isBeingPresented && !presented.isBeingDismissed;
206
242
 
207
- // Treat position changes as transitioning when another controller is presented on top
208
- [self emitChangePositionDelegateWithPosition:self.currentPosition
209
- transitioning:_layoutTransitioning || !self.isTopmostPresentedController];
243
+ if (!_isDragging) {
244
+ dispatch_async(dispatch_get_main_queue(), ^{
245
+ // Update stored position for current detent (handles content size changes)
246
+ [self storeResolvedPositionForIndex:[self currentDetentIndex]];
210
247
 
211
- // On iOS 26, this is called twice when we have a ScrollView
212
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
213
- self->_layoutTransitioning = NO;
248
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:hasPresentedController];
249
+ });
250
+ }
251
+
252
+ // Emit pending detent change after programmatic resize settles
253
+ if (_pendingDetentIndex >= 0) {
254
+ NSInteger pendingIndex = _pendingDetentIndex;
255
+ _pendingDetentIndex = -1;
256
+
257
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
258
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeDetent:position:detent:)]) {
259
+ CGFloat detent = [self detentValueForIndex:pendingIndex];
260
+ [self.delegate viewControllerDidChangeDetent:pendingIndex position:self.currentPosition detent:detent];
261
+ }
214
262
  });
215
263
  }
216
264
  }
@@ -237,13 +285,28 @@
237
285
  if (!presentedView)
238
286
  return;
239
287
 
288
+ // Disable pan gestures if draggable is NO
289
+ if (!self.draggable) {
290
+ [GestureUtil setPanGesturesEnabled:NO forView:presentedView];
291
+
292
+ // Also disable ScrollView's pan gesture if present
293
+ TrueSheetContentView *contentView = [self findContentView:presentedView];
294
+ if (contentView) {
295
+ RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
296
+ if (scrollViewComponent && scrollViewComponent.scrollView) {
297
+ [GestureUtil setPanGesturesEnabled:NO forView:scrollViewComponent.scrollView];
298
+ }
299
+ }
300
+ return;
301
+ }
302
+
240
303
  // Attach to presented view's pan gesture (sheet's drag gesture from UIKit)
241
304
  [GestureUtil attachPanGestureHandler:presentedView target:self selector:@selector(handlePanGesture:)];
242
305
 
243
306
  // Also attach to ScrollView's pan gesture if present
244
307
  TrueSheetContentView *contentView = [self findContentView:presentedView];
245
308
  if (contentView) {
246
- RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView];
309
+ RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
247
310
  if (scrollViewComponent && scrollViewComponent.scrollView) {
248
311
  [GestureUtil attachPanGestureHandler:scrollViewComponent.scrollView
249
312
  target:self
@@ -252,11 +315,29 @@
252
315
  }
253
316
  }
254
317
 
318
+ - (void)updateDraggable {
319
+ UIView *presentedView = self.presentedView;
320
+ if (!presentedView)
321
+ return;
322
+
323
+ [GestureUtil setPanGesturesEnabled:self.draggable forView:presentedView];
324
+
325
+ // Also update ScrollView's pan gesture if present
326
+ TrueSheetContentView *contentView = [self findContentView:presentedView];
327
+ if (contentView) {
328
+ RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
329
+ if (scrollViewComponent && scrollViewComponent.scrollView) {
330
+ [GestureUtil setPanGesturesEnabled:self.draggable forView:scrollViewComponent.scrollView];
331
+ }
332
+ }
333
+ }
334
+
255
335
  - (void)handlePanGesture:(UIPanGestureRecognizer *)gesture {
256
336
  NSInteger index = [self currentDetentIndex];
337
+ CGFloat detent = [self detentValueForIndex:index];
257
338
 
258
- if ([self.delegate respondsToSelector:@selector(viewControllerDidDrag:index:position:)]) {
259
- [self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition];
339
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidDrag:index:position:detent:)]) {
340
+ [self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition detent:detent];
260
341
  }
261
342
 
262
343
  switch (gesture.state) {
@@ -264,14 +345,19 @@
264
345
  _isDragging = YES;
265
346
  break;
266
347
  case UIGestureRecognizerStateChanged:
267
- if (!_isTrackingPositionFromLayout) {
268
- [self emitChangePositionDelegateWithPosition:self.currentPosition transitioning:NO];
269
- }
348
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES];
270
349
  break;
271
350
  case UIGestureRecognizerStateEnded:
272
- case UIGestureRecognizerStateCancelled:
351
+ case UIGestureRecognizerStateCancelled: {
273
352
  _isDragging = NO;
353
+ dispatch_async(dispatch_get_main_queue(), ^{
354
+ // Store resolved position when drag ends
355
+ [self storeResolvedPositionForIndex:[self currentDetentIndex]];
356
+
357
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO];
358
+ });
274
359
  break;
360
+ }
275
361
  default:
276
362
  break;
277
363
  }
@@ -279,90 +365,156 @@
279
365
 
280
366
  #pragma mark - Position Tracking
281
367
 
282
- - (void)emitChangePositionDelegateWithPosition:(CGFloat)position transitioning:(BOOL)transitioning {
283
- if (_lastPosition != position) {
368
+ - (void)emitChangePositionDelegateWithPosition:(CGFloat)position realtime:(BOOL)realtime {
369
+ // Use epsilon comparison to avoid missing updates due to floating point precision
370
+ if (fabs(_lastPosition - position) > 0.01) {
284
371
  _lastPosition = position;
285
372
 
286
- NSInteger index = [self currentDetentIndex];
287
- if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:transitioning:)]) {
288
- [self.delegate viewControllerDidChangePosition:index position:position transitioning:transitioning];
373
+ CGFloat index = [self interpolatedIndexForPosition:position];
374
+ CGFloat detent = [self interpolatedDetentForPosition:position];
375
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:detent:realtime:)]) {
376
+ [self.delegate viewControllerDidChangePosition:index position:position detent:detent realtime:realtime];
289
377
  }
290
378
  }
291
379
  }
292
380
 
293
- /**
294
- * Sets up position tracking during view controller transitions using a fake view.
295
- *
296
- * This uses a hidden "fake" view added to the container that animates alongside
297
- * the presented view. By observing the presentation layer, we can track smooth
298
- * position changes during native transition animations.
299
- */
300
- - (void)setupTransitionPositionTracking {
301
- if (self.transitionCoordinator == nil)
302
- return;
381
+ /// Stores the current position for the given detent index
382
+ - (void)storeResolvedPositionForIndex:(NSInteger)index {
383
+ if (index >= 0 && index < (NSInteger)_resolvedDetentPositions.count) {
384
+ _resolvedDetentPositions[index] = @(self.currentPosition);
385
+ }
386
+ }
303
387
 
304
- _isTransitioning = YES;
388
+ /// Returns the estimated Y position for a detent index, using stored positions when available
389
+ - (CGFloat)estimatedPositionForIndex:(NSInteger)index {
390
+ if (index < 0 || index >= (NSInteger)_resolvedDetentPositions.count)
391
+ return 0;
305
392
 
306
- UIView *containerView = self.sheetPresentationController.containerView;
307
- UIView *presentedView = self.presentedView;
393
+ CGFloat storedPos = [_resolvedDetentPositions[index] doubleValue];
394
+ if (storedPos > 0) {
395
+ return storedPos;
396
+ }
308
397
 
309
- if (!containerView || !presentedView)
310
- return;
398
+ // Estimate based on detent value and known offset from first resolved position
399
+ CGFloat detentValue = [self detentValueForIndex:index];
400
+ CGFloat basePosition = self.screenHeight - (detentValue * self.screenHeight);
401
+
402
+ // Find a resolved position to calculate offset
403
+ for (NSInteger i = 0; i < (NSInteger)_resolvedDetentPositions.count; i++) {
404
+ CGFloat pos = [_resolvedDetentPositions[i] doubleValue];
405
+ if (pos > 0) {
406
+ CGFloat knownDetent = [self detentValueForIndex:i];
407
+ CGFloat expectedPos = self.screenHeight - (knownDetent * self.screenHeight);
408
+ CGFloat offset = pos - expectedPos;
409
+ return basePosition + offset;
410
+ }
411
+ }
311
412
 
312
- CGRect frame = presentedView.frame;
313
- BOOL isPresenting = self.isBeingPresented;
413
+ return basePosition;
414
+ }
314
415
 
315
- // Set initial position: presenting starts from bottom, dismissing from current
316
- frame.origin.y = isPresenting ? self.containerHeight : presentedView.frame.origin.y;
317
- _fakeTransitionView.frame = frame;
416
+ /// Finds the segment containing the given position and returns the lower index and progress within that segment.
417
+ /// Returns YES if a segment was found, NO otherwise. When NO, `outIndex` contains the boundary index.
418
+ - (BOOL)findSegmentForPosition:(CGFloat)position outIndex:(NSInteger *)outIndex outProgress:(CGFloat *)outProgress {
419
+ NSInteger count = _resolvedDetentPositions.count;
420
+ if (count == 0) {
421
+ *outIndex = -1;
422
+ *outProgress = 0;
423
+ return NO;
424
+ }
318
425
 
319
- auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
320
- [[context containerView] addSubview:self->_fakeTransitionView];
426
+ if (count == 1) {
427
+ *outIndex = 0;
428
+ *outProgress = 0;
429
+ return NO;
430
+ }
321
431
 
322
- CGRect finalFrame = presentedView.frame;
323
- finalFrame.origin.y = presentedView.frame.origin.y;
324
- self->_fakeTransitionView.frame = finalFrame;
432
+ CGFloat firstPos = [self estimatedPositionForIndex:0];
433
+ CGFloat lastPos = [self estimatedPositionForIndex:count - 1];
325
434
 
326
- self->_lastTransitionPosition = finalFrame.origin.y;
435
+ // Below first detent (position > firstPos means sheet is smaller)
436
+ if (position > firstPos) {
437
+ CGFloat range = self.screenHeight - firstPos;
438
+ *outIndex = -1;
439
+ *outProgress = range > 0 ? (position - firstPos) / range : 0;
440
+ return NO;
441
+ }
327
442
 
328
- // Track position at screen refresh rate via display link
329
- self->_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(trackTransitionPosition:)];
330
- [self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
331
- };
443
+ // Above last detent
444
+ if (position < lastPos) {
445
+ *outIndex = count - 1;
446
+ *outProgress = 0;
447
+ return NO;
448
+ }
449
+
450
+ // Find segment (positions decrease as index increases)
451
+ for (NSInteger i = 0; i < count - 1; i++) {
452
+ CGFloat pos = [self estimatedPositionForIndex:i];
453
+ CGFloat nextPos = [self estimatedPositionForIndex:i + 1];
332
454
 
333
- [self.transitionCoordinator
334
- animateAlongsideTransition:animation
335
- completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
336
- [self->_displayLink invalidate];
337
- self->_displayLink = nil;
338
- [self->_fakeTransitionView removeFromSuperview];
339
- self->_isTransitioning = NO;
340
- }];
455
+ if (position <= pos && position >= nextPos) {
456
+ CGFloat range = pos - nextPos;
457
+ *outIndex = i;
458
+ *outProgress = range > 0 ? (pos - position) / range : 0;
459
+ return YES;
460
+ }
461
+ }
462
+
463
+ *outIndex = count - 1;
464
+ *outProgress = 0;
465
+ return NO;
341
466
  }
342
467
 
343
- - (void)trackTransitionPosition:(CADisplayLink *)displayLink {
344
- UIView *presentedView = self.presentedView;
468
+ - (CGFloat)interpolatedIndexForPosition:(CGFloat)position {
469
+ NSInteger index;
470
+ CGFloat progress;
471
+ BOOL found = [self findSegmentForPosition:position outIndex:&index outProgress:&progress];
345
472
 
346
- if (_isDragging || !_fakeTransitionView || !presentedView)
347
- return;
473
+ if (!found) {
474
+ if (index == -1) {
475
+ // Below first detent - return negative progress
476
+ return -progress;
477
+ }
478
+ // At or beyond boundary
479
+ return index;
480
+ }
348
481
 
349
- // Presentation layer contains in-flight animated values (not final/target values)
350
- CALayer *presentationLayer = _fakeTransitionView.layer.presentationLayer;
482
+ // Within a segment - interpolate
483
+ return index + fmax(0, fmin(1, progress));
484
+ }
351
485
 
352
- if (presentationLayer) {
353
- BOOL transitioning = NO;
354
- CGFloat position = presentationLayer.frame.origin.y;
486
+ - (CGFloat)interpolatedDetentForPosition:(CGFloat)position {
487
+ NSInteger index;
488
+ CGFloat progress;
489
+ BOOL found = [self findSegmentForPosition:position outIndex:&index outProgress:&progress];
355
490
 
356
- // If position matches last transition position (within epsilon), sheet is repositioning after drag
357
- if (fabs(_lastTransitionPosition - position) < 0.5) {
358
- transitioning = YES;
359
- position = presentedView.frame.origin.y;
360
- } else {
361
- _lastTransitionPosition = position;
491
+ if (!found) {
492
+ if (index == -1) {
493
+ // Below first detent
494
+ CGFloat firstDetent = [self detentValueForIndex:0];
495
+ return fmax(0, firstDetent * (1 - progress));
362
496
  }
497
+ // At or beyond boundary
498
+ return [self detentValueForIndex:index];
499
+ }
500
+
501
+ // Within a segment - interpolate between detent values
502
+ CGFloat detent = [self detentValueForIndex:index];
503
+ CGFloat nextDetent = [self detentValueForIndex:index + 1];
504
+ return detent + progress * (nextDetent - detent);
505
+ }
363
506
 
364
- [self emitChangePositionDelegateWithPosition:position transitioning:transitioning];
507
+ - (CGFloat)detentValueForIndex:(NSInteger)index {
508
+ if (index >= 0 && index < (NSInteger)_detents.count) {
509
+ CGFloat value = [_detents[index] doubleValue];
510
+ // For auto (-1), calculate actual fraction from content + header height
511
+ if (value == -1) {
512
+ CGFloat autoHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue];
513
+ return autoHeight / self.screenHeight;
514
+ }
515
+ return value;
365
516
  }
517
+ return 0;
366
518
  }
367
519
 
368
520
  #pragma mark - Sheet Configuration
@@ -373,16 +525,18 @@
373
525
  return;
374
526
 
375
527
  NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
528
+ [_resolvedDetentPositions removeAllObjects];
376
529
 
377
- // Subtract bottomInset to prevent iOS from adding extra bottom insets
378
- CGFloat totalHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue] - self.bottomInset;
530
+ CGFloat autoHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue];
379
531
 
380
532
  for (NSInteger index = 0; index < self.detents.count; index++) {
381
533
  id detent = self.detents[index];
382
534
  UISheetPresentationControllerDetent *sheetDetent = [self detentForValue:detent
383
- withHeight:totalHeight
535
+ withAutoHeight:autoHeight
384
536
  atIndex:index];
385
537
  [detents addObject:sheetDetent];
538
+ // Initialize with placeholder - will be updated when sheet settles at each detent
539
+ [_resolvedDetentPositions addObject:@(0)];
386
540
  }
387
541
 
388
542
  sheet.detents = detents;
@@ -408,25 +562,19 @@
408
562
  }
409
563
  }
410
564
 
411
- - (UISheetPresentationControllerDetent *)detentForValue:(id)detent withHeight:(CGFloat)height atIndex:(NSInteger)index {
565
+ - (UISheetPresentationControllerDetent *)detentForValue:(id)detent
566
+ withAutoHeight:(CGFloat)autoHeight
567
+ atIndex:(NSInteger)index {
412
568
  if (![detent isKindOfClass:[NSNumber class]]) {
413
569
  return [UISheetPresentationControllerDetent mediumDetent];
414
570
  }
415
571
 
416
- CGFloat value = [detent floatValue];
572
+ CGFloat value = [detent doubleValue];
417
573
 
418
574
  // -1 represents "auto" (fit content height)
419
575
  if (value == -1) {
420
576
  if (@available(iOS 16.0, *)) {
421
- NSString *detentId = @"custom-auto";
422
- return [UISheetPresentationControllerDetent
423
- customDetentWithIdentifier:detentId
424
- resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
425
- CGFloat maxDetentValue = context.maximumDetentValue;
426
- CGFloat maxValue =
427
- self.maxHeight ? fmin(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
428
- return fmin(height, maxValue);
429
- }];
577
+ return [self customDetentWithIdentifier:@"custom-auto" height:autoHeight];
430
578
  } else {
431
579
  return [UISheetPresentationControllerDetent mediumDetent];
432
580
  }
@@ -438,21 +586,9 @@
438
586
  }
439
587
 
440
588
  if (@available(iOS 16.0, *)) {
441
- if (value == 1.0) {
442
- return [UISheetPresentationControllerDetent largeDetent];
443
- } else if (value == 0.5) {
444
- return [UISheetPresentationControllerDetent mediumDetent];
445
- } else {
446
- NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
447
- return [UISheetPresentationControllerDetent
448
- customDetentWithIdentifier:detentId
449
- resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
450
- CGFloat maxDetentValue = context.maximumDetentValue;
451
- CGFloat maxValue =
452
- self.maxHeight ? fmin(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
453
- return fmin(value * maxDetentValue, maxValue);
454
- }];
455
- }
589
+ NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
590
+ CGFloat sheetHeight = value * self.screenHeight;
591
+ return [self customDetentWithIdentifier:detentId height:sheetHeight];
456
592
  } else if (value >= 0.5) {
457
593
  return [UISheetPresentationControllerDetent largeDetent];
458
594
  } else {
@@ -460,6 +596,18 @@
460
596
  }
461
597
  }
462
598
 
599
+ - (UISheetPresentationControllerDetent *)customDetentWithIdentifier:(NSString *)identifier
600
+ height:(CGFloat)height API_AVAILABLE(ios(16.0)) {
601
+ return [UISheetPresentationControllerDetent
602
+ customDetentWithIdentifier:identifier
603
+ resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
604
+ CGFloat maxDetentValue = context.maximumDetentValue;
605
+ CGFloat maxValue =
606
+ self.maxHeight ? fmin(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
607
+ return fmin(height, maxValue);
608
+ }];
609
+ }
610
+
463
611
  - (UISheetPresentationControllerDetentIdentifier)detentIdentifierForIndex:(NSInteger)index {
464
612
  UISheetPresentationController *sheet = self.sheetPresentationController;
465
613
  if (!sheet)
@@ -512,6 +660,16 @@
512
660
  [self applyActiveDetent];
513
661
  }
514
662
 
663
+ - (void)resizeToDetentIndex:(NSInteger)index {
664
+ if (index == _activeDetentIndex) {
665
+ return;
666
+ }
667
+
668
+ _pendingDetentIndex = index;
669
+ _activeDetentIndex = index;
670
+ [self applyActiveDetent];
671
+ }
672
+
515
673
  - (void)setupSheetProps {
516
674
  UISheetPresentationController *sheet = self.sheetPresentationController;
517
675
  if (!sheet) {
@@ -527,7 +685,6 @@
527
685
  }
528
686
 
529
687
  sheet.prefersEdgeAttachedInCompactHeight = YES;
530
- sheet.prefersGrabberVisible = self.grabber;
531
688
 
532
689
  if (self.cornerRadius) {
533
690
  sheet.preferredCornerRadius = [self.cornerRadius floatValue];
@@ -537,19 +694,44 @@
537
694
 
538
695
  self.view.backgroundColor = self.backgroundColor;
539
696
 
540
- // Setup or remove blur effect
697
+ // Setup blur effect view - recreate only when blurTint changes
698
+ BOOL blurTintChanged = ![_blurView.blurTint isEqualToString:self.blurTint];
699
+
700
+ if (_blurView && blurTintChanged) {
701
+ [_blurView removeFromSuperview];
702
+ _blurView = nil;
703
+ }
704
+
541
705
  if (self.blurTint && self.blurTint.length > 0) {
542
- UIBlurEffectStyle style = [ConversionUtil blurEffectStyleFromString:self.blurTint];
543
- UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:style]];
544
- blurView.frame = self.view.bounds;
545
- blurView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
546
- [self.view insertSubview:blurView atIndex:0];
547
- } else {
548
- for (UIView *subview in self.view.subviews) {
549
- if ([subview isKindOfClass:[UIVisualEffectView class]]) {
550
- [subview removeFromSuperview];
551
- }
706
+ if (!_blurView) {
707
+ _blurView = [[TrueSheetBlurView alloc] init];
708
+ [_blurView addToView:self.view];
552
709
  }
710
+ _blurView.blurTint = self.blurTint;
711
+ _blurView.blurIntensity = self.blurIntensity;
712
+ _blurView.blurInteraction = self.blurInteraction;
713
+ [_blurView applyBlurEffect];
714
+ }
715
+
716
+ // Setup grabber
717
+ BOOL showGrabber = self.grabber && self.draggable;
718
+
719
+ if (self.grabberOptions) {
720
+ // Use custom grabber view when options are provided
721
+ sheet.prefersGrabberVisible = NO;
722
+
723
+ NSDictionary *options = self.grabberOptions;
724
+ _grabberView.grabberWidth = options[@"width"];
725
+ _grabberView.grabberHeight = options[@"height"];
726
+ _grabberView.topMargin = options[@"topMargin"];
727
+ _grabberView.cornerRadius = options[@"cornerRadius"];
728
+ _grabberView.color = options[@"color"];
729
+ [_grabberView applyConfiguration];
730
+ _grabberView.hidden = !showGrabber;
731
+ } else {
732
+ // Use system default grabber when no options provided
733
+ sheet.prefersGrabberVisible = showGrabber;
734
+ _grabberView.hidden = YES;
553
735
  }
554
736
  }
555
737
 
@@ -557,9 +739,14 @@
557
739
 
558
740
  - (void)sheetPresentationControllerDidChangeSelectedDetentIdentifier:
559
741
  (UISheetPresentationController *)sheetPresentationController {
560
- NSInteger index = [self currentDetentIndex];
561
- if (index >= 0 && [self.delegate respondsToSelector:@selector(viewControllerDidChangeDetent:position:)]) {
562
- [self.delegate viewControllerDidChangeDetent:index position:self.currentPosition];
742
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeDetent:position:detent:)]) {
743
+ dispatch_async(dispatch_get_main_queue(), ^{
744
+ NSInteger index = [self currentDetentIndex];
745
+ if (index >= 0) {
746
+ CGFloat detent = [self detentValueForIndex:index];
747
+ [self.delegate viewControllerDidChangeDetent:index position:self.currentPosition detent:detent];
748
+ }
749
+ });
563
750
  }
564
751
  }
565
752