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