@lodev09/react-native-true-sheet 3.0.0-beta.13 → 3.0.0-beta.15

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 (74) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +20 -20
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +49 -14
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +10 -1
  4. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDialogObserver.kt +17 -43
  5. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetDragEvents.kt +71 -0
  6. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetFocusEvents.kt +65 -0
  7. package/android/src/main/java/com/lodev09/truesheet/events/TrueSheetLifecycleEvents.kt +94 -0
  8. package/android/src/main/java/com/lodev09/truesheet/events/{PositionChangeEvent.kt → TrueSheetStateEvents.kt} +25 -3
  9. package/ios/TrueSheetView.mm +39 -25
  10. package/ios/TrueSheetViewController.h +7 -1
  11. package/ios/TrueSheetViewController.mm +101 -51
  12. package/ios/core/TrueSheetBlurView.h +24 -0
  13. package/ios/{utils/ConversionUtil.mm → core/TrueSheetBlurView.mm} +65 -3
  14. package/ios/events/TrueSheetDragEvents.h +39 -0
  15. package/ios/events/TrueSheetDragEvents.mm +62 -0
  16. package/ios/events/{OnPositionChangeEvent.h → TrueSheetFocusEvents.h} +8 -6
  17. package/ios/events/TrueSheetFocusEvents.mm +49 -0
  18. package/ios/events/TrueSheetLifecycleEvents.h +40 -0
  19. package/ios/events/TrueSheetLifecycleEvents.mm +71 -0
  20. package/ios/events/TrueSheetStateEvents.h +35 -0
  21. package/ios/events/TrueSheetStateEvents.mm +49 -0
  22. package/ios/utils/GestureUtil.h +7 -0
  23. package/ios/utils/GestureUtil.mm +12 -0
  24. package/lib/module/TrueSheet.js +12 -0
  25. package/lib/module/TrueSheet.js.map +1 -1
  26. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +8 -1
  27. package/lib/module/reanimated/ReanimatedTrueSheet.js +6 -6
  28. package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -1
  29. package/lib/typescript/src/TrueSheet.d.ts +2 -0
  30. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  31. package/lib/typescript/src/TrueSheet.types.d.ts +37 -3
  32. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  33. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +6 -1
  34. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/TrueSheet.tsx +16 -0
  37. package/src/TrueSheet.types.ts +42 -3
  38. package/src/fabric/TrueSheetViewNativeComponent.ts +8 -1
  39. package/src/reanimated/ReanimatedTrueSheet.tsx +6 -6
  40. package/android/src/main/java/com/lodev09/truesheet/events/BlurEvent.kt +0 -20
  41. package/android/src/main/java/com/lodev09/truesheet/events/DetentChangeEvent.kt +0 -27
  42. package/android/src/main/java/com/lodev09/truesheet/events/DidDismissEvent.kt +0 -20
  43. package/android/src/main/java/com/lodev09/truesheet/events/DidPresentEvent.kt +0 -27
  44. package/android/src/main/java/com/lodev09/truesheet/events/DragBeginEvent.kt +0 -27
  45. package/android/src/main/java/com/lodev09/truesheet/events/DragChangeEvent.kt +0 -27
  46. package/android/src/main/java/com/lodev09/truesheet/events/DragEndEvent.kt +0 -27
  47. package/android/src/main/java/com/lodev09/truesheet/events/FocusEvent.kt +0 -20
  48. package/android/src/main/java/com/lodev09/truesheet/events/MountEvent.kt +0 -20
  49. package/android/src/main/java/com/lodev09/truesheet/events/WillDismissEvent.kt +0 -20
  50. package/android/src/main/java/com/lodev09/truesheet/events/WillPresentEvent.kt +0 -27
  51. package/ios/events/OnDetentChangeEvent.h +0 -29
  52. package/ios/events/OnDetentChangeEvent.mm +0 -32
  53. package/ios/events/OnDidBlurEvent.h +0 -26
  54. package/ios/events/OnDidBlurEvent.mm +0 -25
  55. package/ios/events/OnDidDismissEvent.h +0 -26
  56. package/ios/events/OnDidDismissEvent.mm +0 -25
  57. package/ios/events/OnDidFocusEvent.h +0 -26
  58. package/ios/events/OnDidFocusEvent.mm +0 -25
  59. package/ios/events/OnDidPresentEvent.h +0 -29
  60. package/ios/events/OnDidPresentEvent.mm +0 -32
  61. package/ios/events/OnDragBeginEvent.h +0 -29
  62. package/ios/events/OnDragBeginEvent.mm +0 -32
  63. package/ios/events/OnDragChangeEvent.h +0 -29
  64. package/ios/events/OnDragChangeEvent.mm +0 -32
  65. package/ios/events/OnDragEndEvent.h +0 -29
  66. package/ios/events/OnDragEndEvent.mm +0 -32
  67. package/ios/events/OnMountEvent.h +0 -26
  68. package/ios/events/OnMountEvent.mm +0 -25
  69. package/ios/events/OnPositionChangeEvent.mm +0 -34
  70. package/ios/events/OnWillDismissEvent.h +0 -26
  71. package/ios/events/OnWillDismissEvent.mm +0 -25
  72. package/ios/events/OnWillPresentEvent.h +0 -29
  73. package/ios/events/OnWillPresentEvent.mm +0 -32
  74. package/ios/utils/ConversionUtil.h +0 -24
@@ -4,9 +4,31 @@ import com.facebook.react.bridge.Arguments
4
4
  import com.facebook.react.bridge.WritableMap
5
5
  import com.facebook.react.uimanager.events.Event
6
6
 
7
+ /**
8
+ * Fired when the detent changes
9
+ * Payload: { index: number, position: number, detent: number }
10
+ */
11
+ class DetentChangeEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
12
+ Event<DetentChangeEvent>(surfaceId, viewId) {
13
+
14
+ override fun getEventName(): String = EVENT_NAME
15
+
16
+ override fun getEventData(): WritableMap =
17
+ Arguments.createMap().apply {
18
+ putInt("index", index)
19
+ putDouble("position", position.toDouble())
20
+ putDouble("detent", detent.toDouble())
21
+ }
22
+
23
+ companion object {
24
+ const val EVENT_NAME = "topDetentChange"
25
+ const val REGISTRATION_NAME = "onDetentChange"
26
+ }
27
+ }
28
+
7
29
  /**
8
30
  * Fired continuously for position updates during drag and animation
9
- * Payload: { index: number, position: number, detent: number, transitioning: boolean }
31
+ * Payload: { index: number, position: number, detent: number, realtime: boolean }
10
32
  */
11
33
  class PositionChangeEvent(
12
34
  surfaceId: Int,
@@ -14,7 +36,7 @@ class PositionChangeEvent(
14
36
  private val index: Float,
15
37
  private val position: Float,
16
38
  private val detent: Float,
17
- private val transitioning: Boolean = false
39
+ private val realtime: Boolean = false
18
40
  ) : Event<PositionChangeEvent>(surfaceId, viewId) {
19
41
 
20
42
  override fun getEventName(): String = EVENT_NAME
@@ -24,7 +46,7 @@ class PositionChangeEvent(
24
46
  putDouble("index", index.toDouble())
25
47
  putDouble("position", position.toDouble())
26
48
  putDouble("detent", detent.toDouble())
27
- putBoolean("transitioning", transitioning)
49
+ putBoolean("realtime", realtime)
28
50
  }
29
51
 
30
52
  companion object {
@@ -14,18 +14,10 @@
14
14
  #import "TrueSheetFooterView.h"
15
15
  #import "TrueSheetModule.h"
16
16
  #import "TrueSheetViewController.h"
17
- #import "events/OnDetentChangeEvent.h"
18
- #import "events/OnDidBlurEvent.h"
19
- #import "events/OnDidDismissEvent.h"
20
- #import "events/OnDidFocusEvent.h"
21
- #import "events/OnDidPresentEvent.h"
22
- #import "events/OnDragBeginEvent.h"
23
- #import "events/OnDragChangeEvent.h"
24
- #import "events/OnDragEndEvent.h"
25
- #import "events/OnMountEvent.h"
26
- #import "events/OnPositionChangeEvent.h"
27
- #import "events/OnWillDismissEvent.h"
28
- #import "events/OnWillPresentEvent.h"
17
+ #import "events/TrueSheetDragEvents.h"
18
+ #import "events/TrueSheetFocusEvents.h"
19
+ #import "events/TrueSheetLifecycleEvents.h"
20
+ #import "events/TrueSheetStateEvents.h"
29
21
  #import "utils/LayoutUtil.h"
30
22
  #import "utils/WindowUtil.h"
31
23
 
@@ -129,6 +121,12 @@ using namespace facebook::react;
129
121
  // Blur tint
130
122
  _controller.blurTint = !newProps.blurTint.empty() ? RCTNSStringFromString(newProps.blurTint) : nil;
131
123
 
124
+ // Blur intensity (-1 means use system default)
125
+ _controller.blurIntensity = newProps.blurIntensity >= 0 ? @(newProps.blurIntensity) : nil;
126
+
127
+ // Blur interaction
128
+ _controller.blurInteraction = newProps.blurInteraction;
129
+
132
130
  // Corner radius
133
131
  _controller.cornerRadius = newProps.cornerRadius < 0 ? nil : @(newProps.cornerRadius);
134
132
 
@@ -140,6 +138,7 @@ using namespace facebook::react;
140
138
  _controller.grabber = newProps.grabber;
141
139
  _controller.pageSizing = newProps.pageSizing;
142
140
  _controller.modalInPresentation = !newProps.dismissible;
141
+ _controller.draggable = newProps.draggable;
143
142
  _controller.dimmed = newProps.dimmed;
144
143
 
145
144
  if (newProps.dimmedDetentIndex >= 0) {
@@ -195,6 +194,7 @@ using namespace facebook::react;
195
194
  [self->_controller setupSheetDetents];
196
195
  [self->_controller applyActiveDetent];
197
196
  }];
197
+ [_controller updateDraggable];
198
198
  } else if (_initialDetentIndex >= 0) {
199
199
  [self presentAtIndex:_initialDetentIndex animated:_initialDetentAnimated completion:nil];
200
200
  }
@@ -244,7 +244,7 @@ using namespace facebook::react;
244
244
  _containerView.scrollViewPinningEnabled = _scrollable;
245
245
  [_containerView setupContentScrollViewPinning];
246
246
 
247
- [OnMountEvent emit:_eventEmitter];
247
+ [TrueSheetLifecycleEvents emitMount:_eventEmitter];
248
248
  }
249
249
 
250
250
  - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
@@ -372,27 +372,33 @@ using namespace facebook::react;
372
372
  NSInteger index = [_controller currentDetentIndex];
373
373
  _controller.activeDetentIndex = index;
374
374
  CGFloat detent = [_controller detentValueForIndex:index];
375
- [OnWillPresentEvent emit:_eventEmitter index:index position:_controller.currentPosition detent:detent];
375
+ [TrueSheetLifecycleEvents emitWillPresent:_eventEmitter
376
+ index:index
377
+ position:_controller.currentPosition
378
+ detent:detent];
376
379
  }
377
380
 
378
381
  - (void)viewControllerDidPresent {
379
382
  NSInteger index = [_controller currentDetentIndex];
380
383
  CGFloat detent = [_controller detentValueForIndex:index];
381
- [OnDidPresentEvent emit:_eventEmitter index:index position:_controller.currentPosition detent:detent];
384
+ [TrueSheetLifecycleEvents emitDidPresent:_eventEmitter
385
+ index:index
386
+ position:_controller.currentPosition
387
+ detent:detent];
382
388
  }
383
389
 
384
390
  - (void)viewControllerDidDrag:(UIGestureRecognizerState)state index:(NSInteger)index position:(CGFloat)position {
385
391
  CGFloat detent = [_controller detentValueForIndex:index];
386
392
  switch (state) {
387
393
  case UIGestureRecognizerStateBegan:
388
- [OnDragBeginEvent emit:_eventEmitter index:index position:position detent:detent];
394
+ [TrueSheetDragEvents emitDragBegin:_eventEmitter index:index position:position detent:detent];
389
395
  break;
390
396
  case UIGestureRecognizerStateChanged:
391
- [OnDragChangeEvent emit:_eventEmitter index:index position:position detent:detent];
397
+ [TrueSheetDragEvents emitDragChange:_eventEmitter index:index position:position detent:detent];
392
398
  break;
393
399
  case UIGestureRecognizerStateEnded:
394
400
  case UIGestureRecognizerStateCancelled:
395
- [OnDragEndEvent emit:_eventEmitter index:index position:position detent:detent];
401
+ [TrueSheetDragEvents emitDragEnd:_eventEmitter index:index position:position detent:detent];
396
402
  break;
397
403
  default:
398
404
  break;
@@ -400,12 +406,12 @@ using namespace facebook::react;
400
406
  }
401
407
 
402
408
  - (void)viewControllerWillDismiss {
403
- [OnWillDismissEvent emit:_eventEmitter];
409
+ [TrueSheetLifecycleEvents emitWillDismiss:_eventEmitter];
404
410
  }
405
411
 
406
412
  - (void)viewControllerDidDismiss {
407
413
  _controller.activeDetentIndex = -1;
408
- [OnDidDismissEvent emit:_eventEmitter];
414
+ [TrueSheetLifecycleEvents emitDidDismiss:_eventEmitter];
409
415
  }
410
416
 
411
417
  - (void)viewControllerDidChangeDetent:(NSInteger)index position:(CGFloat)position {
@@ -413,26 +419,34 @@ using namespace facebook::react;
413
419
  _controller.activeDetentIndex = index;
414
420
  }
415
421
  CGFloat detent = [_controller detentValueForIndex:index];
416
- [OnDetentChangeEvent emit:_eventEmitter index:index position:position detent:detent];
422
+ [TrueSheetStateEvents emitDetentChange:_eventEmitter index:index position:position detent:detent];
417
423
  }
418
424
 
419
425
  - (void)viewControllerDidChangePosition:(CGFloat)index
420
426
  position:(CGFloat)position
421
427
  detent:(CGFloat)detent
422
- transitioning:(BOOL)transitioning {
423
- [OnPositionChangeEvent emit:_eventEmitter index:index position:position detent:detent transitioning:transitioning];
428
+ realtime:(BOOL)realtime {
429
+ [TrueSheetStateEvents emitPositionChange:_eventEmitter index:index position:position detent:detent realtime:realtime];
424
430
  }
425
431
 
426
432
  - (void)viewControllerDidChangeSize:(CGSize)size {
427
433
  [self updateStateWithSize:size];
428
434
  }
429
435
 
436
+ - (void)viewControllerWillFocus {
437
+ [TrueSheetFocusEvents emitWillFocus:_eventEmitter];
438
+ }
439
+
430
440
  - (void)viewControllerDidFocus {
431
- [OnDidFocusEvent emit:_eventEmitter];
441
+ [TrueSheetFocusEvents emitDidFocus:_eventEmitter];
442
+ }
443
+
444
+ - (void)viewControllerWillBlur {
445
+ [TrueSheetFocusEvents emitWillBlur:_eventEmitter];
432
446
  }
433
447
 
434
448
  - (void)viewControllerDidBlur {
435
- [OnDidBlurEvent emit:_eventEmitter];
449
+ [TrueSheetFocusEvents emitDidBlur:_eventEmitter];
436
450
  }
437
451
 
438
452
  #pragma mark - Private Helpers
@@ -28,9 +28,11 @@ NS_ASSUME_NONNULL_BEGIN
28
28
  - (void)viewControllerDidChangePosition:(CGFloat)index
29
29
  position:(CGFloat)position
30
30
  detent:(CGFloat)detent
31
- transitioning:(BOOL)transitioning;
31
+ realtime:(BOOL)realtime;
32
32
  - (void)viewControllerDidChangeSize:(CGSize)size;
33
+ - (void)viewControllerWillFocus;
33
34
  - (void)viewControllerDidFocus;
35
+ - (void)viewControllerWillBlur;
34
36
  - (void)viewControllerDidBlur;
35
37
 
36
38
  @end
@@ -50,9 +52,12 @@ NS_ASSUME_NONNULL_BEGIN
50
52
  @property (nonatomic, strong, nullable) UIColor *backgroundColor;
51
53
  @property (nonatomic, strong, nullable) NSNumber *cornerRadius;
52
54
  @property (nonatomic, assign) BOOL grabber;
55
+ @property (nonatomic, assign) BOOL draggable;
53
56
  @property (nonatomic, assign) BOOL dimmed;
54
57
  @property (nonatomic, strong, nullable) NSNumber *dimmedDetentIndex;
55
58
  @property (nonatomic, copy, nullable) NSString *blurTint;
59
+ @property (nonatomic, strong, nullable) NSNumber *blurIntensity;
60
+ @property (nonatomic, assign) BOOL blurInteraction;
56
61
  @property (nonatomic, assign) BOOL pageSizing;
57
62
  @property (nonatomic, assign) BOOL layoutTransitioning;
58
63
  @property (nonatomic, assign) BOOL isPresented;
@@ -62,6 +67,7 @@ NS_ASSUME_NONNULL_BEGIN
62
67
  - (void)setupActiveDetentWithIndex:(NSInteger)index;
63
68
  - (void)setupSheetDetents;
64
69
  - (void)setupSheetProps;
70
+ - (void)updateDraggable;
65
71
  - (NSInteger)currentDetentIndex;
66
72
  - (CGFloat)currentPosition;
67
73
  - (CGFloat)bottomInset;
@@ -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,10 +21,8 @@
21
21
 
22
22
  @implementation TrueSheetViewController {
23
23
  CGFloat _lastPosition;
24
- CGFloat _lastTransitionPosition;
25
24
  BOOL _isTransitioning;
26
25
  BOOL _isDragging;
27
- BOOL _isTrackingPositionFromLayout;
28
26
 
29
27
  // Hidden view used to track position during native transition animations
30
28
  UIView *_fakeTransitionView;
@@ -32,6 +30,9 @@
32
30
 
33
31
  // Reference to parent TrueSheetViewController (if presented from another sheet)
34
32
  __weak TrueSheetViewController *_parentSheetController;
33
+
34
+ // Blur effect view
35
+ TrueSheetBlurView *_blurView;
35
36
  }
36
37
 
37
38
  #pragma mark - Initialization
@@ -42,14 +43,13 @@
42
43
  _contentHeight = @(0);
43
44
  _headerHeight = @(0);
44
45
  _grabber = YES;
46
+ _draggable = YES;
45
47
  _dimmed = YES;
46
48
  _dimmedDetentIndex = @(0);
47
49
  _pageSizing = YES;
48
50
  _lastPosition = 0;
49
- _lastTransitionPosition = 0;
50
51
  _isTransitioning = NO;
51
52
  _isDragging = NO;
52
- _isTrackingPositionFromLayout = NO;
53
53
  _layoutTransitioning = NO;
54
54
  _isPresented = NO;
55
55
  _activeDetentIndex = -1;
@@ -57,6 +57,8 @@
57
57
  _fakeTransitionView = [[UIView alloc] init];
58
58
  _fakeTransitionView.hidden = YES;
59
59
  _fakeTransitionView.userInteractionEnabled = NO;
60
+
61
+ _blurInteraction = YES;
60
62
  }
61
63
  return self;
62
64
  }
@@ -148,9 +150,9 @@
148
150
  UIViewController *presenter = self.presentingViewController;
149
151
  if ([presenter isKindOfClass:[TrueSheetViewController class]]) {
150
152
  _parentSheetController = (TrueSheetViewController *)presenter;
151
- // Notify parent that it lost focus
152
- if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidBlur)]) {
153
- [_parentSheetController.delegate viewControllerDidBlur];
153
+ // Notify parent that it is about to lose focus
154
+ if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerWillBlur)]) {
155
+ [_parentSheetController.delegate viewControllerWillBlur];
154
156
  }
155
157
  }
156
158
 
@@ -165,6 +167,13 @@
165
167
  [super viewDidAppear:animated];
166
168
 
167
169
  if (!_isPresented) {
170
+ // Notify parent that it has lost focus (after the child sheet appeared)
171
+ if (_parentSheetController) {
172
+ if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerDidBlur)]) {
173
+ [_parentSheetController.delegate viewControllerDidBlur];
174
+ }
175
+ }
176
+
168
177
  if ([self.delegate respondsToSelector:@selector(viewControllerDidPresent)]) {
169
178
  [self.delegate viewControllerDidPresent];
170
179
  }
@@ -176,12 +185,22 @@
176
185
  - (void)viewWillDisappear:(BOOL)animated {
177
186
  [super viewWillDisappear:animated];
178
187
 
179
- if (self.isBeingDismissed && [self.delegate respondsToSelector:@selector(viewControllerWillDismiss)]) {
180
- [self.delegate viewControllerWillDismiss];
188
+ BOOL isActuallyDismissing = self.presentingViewController == nil || self.isBeingDismissed;
189
+
190
+ if (isActuallyDismissing) {
191
+ // Notify the parent sheet (if any) that it is about to regain focus
192
+ if (_parentSheetController) {
193
+ if ([_parentSheetController.delegate respondsToSelector:@selector(viewControllerWillFocus)]) {
194
+ [_parentSheetController.delegate viewControllerWillFocus];
195
+ }
196
+ }
197
+
198
+ if ([self.delegate respondsToSelector:@selector(viewControllerWillDismiss)]) {
199
+ [self.delegate viewControllerWillDismiss];
200
+ }
181
201
  }
182
202
 
183
203
  [self setupTransitionPositionTracking];
184
- _isTrackingPositionFromLayout = NO;
185
204
  }
186
205
 
187
206
  - (void)viewDidDisappear:(BOOL)animated {
@@ -204,8 +223,6 @@
204
223
  }
205
224
  }
206
225
 
207
- _isTrackingPositionFromLayout = NO;
208
-
209
226
  if (isActuallyDismissing) {
210
227
  _isPresented = NO;
211
228
  _activeDetentIndex = -1;
@@ -219,12 +236,9 @@
219
236
  [self.delegate viewControllerDidChangeSize:self.view.frame.size];
220
237
  }
221
238
 
222
- if (!_isTransitioning && self.isActiveAndVisible) {
223
- _isTrackingPositionFromLayout = YES;
224
-
225
- // Treat position changes as transitioning when another controller is presented on top
226
- [self emitChangePositionDelegateWithPosition:self.currentPosition
227
- transitioning:_layoutTransitioning || !self.isTopmostPresentedController];
239
+ if (!_isTransitioning && !_isDragging && self.isActiveAndVisible) {
240
+ // Not realtime when layout changes (e.g., another controller is presented on top)
241
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO];
228
242
 
229
243
  // On iOS 26, this is called twice when we have a ScrollView
230
244
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@@ -255,6 +269,21 @@
255
269
  if (!presentedView)
256
270
  return;
257
271
 
272
+ // Disable pan gestures if draggable is NO
273
+ if (!self.draggable) {
274
+ [GestureUtil setPanGesturesEnabled:NO forView:presentedView];
275
+
276
+ // Also disable ScrollView's pan gesture if present
277
+ TrueSheetContentView *contentView = [self findContentView:presentedView];
278
+ if (contentView) {
279
+ RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
280
+ if (scrollViewComponent && scrollViewComponent.scrollView) {
281
+ [GestureUtil setPanGesturesEnabled:NO forView:scrollViewComponent.scrollView];
282
+ }
283
+ }
284
+ return;
285
+ }
286
+
258
287
  // Attach to presented view's pan gesture (sheet's drag gesture from UIKit)
259
288
  [GestureUtil attachPanGestureHandler:presentedView target:self selector:@selector(handlePanGesture:)];
260
289
 
@@ -270,6 +299,23 @@
270
299
  }
271
300
  }
272
301
 
302
+ - (void)updateDraggable {
303
+ UIView *presentedView = self.presentedView;
304
+ if (!presentedView)
305
+ return;
306
+
307
+ [GestureUtil setPanGesturesEnabled:self.draggable forView:presentedView];
308
+
309
+ // Also update ScrollView's pan gesture if present
310
+ TrueSheetContentView *contentView = [self findContentView:presentedView];
311
+ if (contentView) {
312
+ RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView:nil];
313
+ if (scrollViewComponent && scrollViewComponent.scrollView) {
314
+ [GestureUtil setPanGesturesEnabled:self.draggable forView:scrollViewComponent.scrollView];
315
+ }
316
+ }
317
+ }
318
+
273
319
  - (void)handlePanGesture:(UIPanGestureRecognizer *)gesture {
274
320
  NSInteger index = [self currentDetentIndex];
275
321
 
@@ -282,14 +328,16 @@
282
328
  _isDragging = YES;
283
329
  break;
284
330
  case UIGestureRecognizerStateChanged:
285
- if (!_isTrackingPositionFromLayout) {
286
- [self emitChangePositionDelegateWithPosition:self.currentPosition transitioning:NO];
287
- }
331
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES];
288
332
  break;
289
333
  case UIGestureRecognizerStateEnded:
290
- case UIGestureRecognizerStateCancelled:
291
- _isDragging = NO;
334
+ case UIGestureRecognizerStateCancelled: {
335
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO];
336
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
337
+ self->_isDragging = NO;
338
+ });
292
339
  break;
340
+ }
293
341
  default:
294
342
  break;
295
343
  }
@@ -297,15 +345,16 @@
297
345
 
298
346
  #pragma mark - Position Tracking
299
347
 
300
- - (void)emitChangePositionDelegateWithPosition:(CGFloat)position transitioning:(BOOL)transitioning {
301
- if (_lastPosition != position) {
348
+ - (void)emitChangePositionDelegateWithPosition:(CGFloat)position realtime:(BOOL)realtime {
349
+ // Use epsilon comparison to avoid missing updates due to floating point precision
350
+ if (fabs(_lastPosition - position) > 0.01) {
302
351
  _lastPosition = position;
303
352
 
304
353
  CGFloat index = [self interpolatedIndexForPosition:position];
305
354
  NSInteger discreteIndex = (NSInteger)round(index);
306
355
  CGFloat detent = [self detentValueForIndex:discreteIndex];
307
- if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:detent:transitioning:)]) {
308
- [self.delegate viewControllerDidChangePosition:index position:position detent:detent transitioning:transitioning];
356
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:detent:realtime:)]) {
357
+ [self.delegate viewControllerDidChangePosition:index position:position detent:detent realtime:realtime];
309
358
  }
310
359
  }
311
360
  }
@@ -395,8 +444,6 @@
395
444
  finalFrame.origin.y = presentedView.frame.origin.y;
396
445
  self->_fakeTransitionView.frame = finalFrame;
397
446
 
398
- self->_lastTransitionPosition = finalFrame.origin.y;
399
-
400
447
  // Track position at screen refresh rate via display link
401
448
  self->_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(trackTransitionPosition:)];
402
449
  [self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
@@ -422,18 +469,12 @@
422
469
  CALayer *presentationLayer = _fakeTransitionView.layer.presentationLayer;
423
470
 
424
471
  if (presentationLayer) {
425
- BOOL transitioning = NO;
426
- CGFloat position = presentationLayer.frame.origin.y;
472
+ CGFloat transitioningPosition = presentationLayer.frame.origin.y;
473
+ CGFloat staticPosition = _fakeTransitionView.frame.origin.y;
427
474
 
428
- // If position matches last transition position (within epsilon), sheet is repositioning after drag
429
- if (fabs(_lastTransitionPosition - position) < 0.5) {
430
- transitioning = YES;
431
- position = presentedView.frame.origin.y;
432
- } else {
433
- _lastTransitionPosition = position;
475
+ if (fabs(staticPosition - transitioningPosition) > 0.01) {
476
+ [self emitChangePositionDelegateWithPosition:transitioningPosition realtime:YES];
434
477
  }
435
-
436
- [self emitChangePositionDelegateWithPosition:position transitioning:transitioning];
437
478
  }
438
479
  }
439
480
 
@@ -447,7 +488,7 @@
447
488
  NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
448
489
 
449
490
  // Subtract bottomInset to prevent iOS from adding extra bottom insets
450
- CGFloat autoHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue] - self.bottomInset;
491
+ CGFloat autoHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue];
451
492
 
452
493
  for (NSInteger index = 0; index < self.detents.count; index++) {
453
494
  id detent = self.detents[index];
@@ -597,7 +638,7 @@
597
638
  }
598
639
 
599
640
  sheet.prefersEdgeAttachedInCompactHeight = YES;
600
- sheet.prefersGrabberVisible = self.grabber;
641
+ sheet.prefersGrabberVisible = self.grabber && self.draggable;
601
642
 
602
643
  if (self.cornerRadius) {
603
644
  sheet.preferredCornerRadius = [self.cornerRadius floatValue];
@@ -609,16 +650,25 @@
609
650
 
610
651
  // Setup or remove blur effect
611
652
  if (self.blurTint && self.blurTint.length > 0) {
612
- UIBlurEffectStyle style = [ConversionUtil blurEffectStyleFromString:self.blurTint];
613
- UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:style]];
614
- blurView.frame = self.view.bounds;
615
- blurView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
616
- [self.view insertSubview:blurView atIndex:0];
653
+ // Create blur view if needed
654
+ if (!_blurView) {
655
+ _blurView = [[TrueSheetBlurView alloc] init];
656
+ _blurView.frame = self.view.bounds;
657
+ _blurView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
658
+ [self.view insertSubview:_blurView atIndex:0];
659
+ }
660
+
661
+ // Update blur properties and apply effect
662
+ _blurView.blurTint = self.blurTint;
663
+ _blurView.blurIntensity = self.blurIntensity;
664
+ _blurView.blurInteraction = self.blurInteraction;
665
+ [_blurView applyBlurEffect];
617
666
  } else {
618
- for (UIView *subview in self.view.subviews) {
619
- if ([subview isKindOfClass:[UIVisualEffectView class]]) {
620
- [subview removeFromSuperview];
621
- }
667
+ // Remove blur effect
668
+ if (_blurView) {
669
+ [_blurView removeBlurEffect];
670
+ [_blurView removeFromSuperview];
671
+ _blurView = nil;
622
672
  }
623
673
  }
624
674
  }
@@ -0,0 +1,24 @@
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 <UIKit/UIKit.h>
10
+
11
+ NS_ASSUME_NONNULL_BEGIN
12
+
13
+ @interface TrueSheetBlurView : UIVisualEffectView
14
+
15
+ @property (nonatomic, copy, nullable) NSString *blurTint;
16
+ @property (nonatomic, strong, nullable) NSNumber *blurIntensity;
17
+ @property (nonatomic, assign) BOOL blurInteraction;
18
+
19
+ - (void)applyBlurEffect;
20
+ - (void)removeBlurEffect;
21
+
22
+ @end
23
+
24
+ NS_ASSUME_NONNULL_END
@@ -6,9 +6,13 @@
6
6
  // LICENSE file in the root directory of this source tree.
7
7
  //
8
8
 
9
- #import "ConversionUtil.h"
9
+ #import "TrueSheetBlurView.h"
10
10
 
11
- @implementation ConversionUtil
11
+ @implementation TrueSheetBlurView {
12
+ UIViewPropertyAnimator *_blurAnimator;
13
+ }
14
+
15
+ #pragma mark - Private
12
16
 
13
17
  + (UIBlurEffectStyle)blurEffectStyleFromString:(NSString *)tintString {
14
18
  static NSDictionary<NSString *, NSNumber *> *styleMap = nil;
@@ -43,8 +47,66 @@
43
47
  return (UIBlurEffectStyle)[style integerValue];
44
48
  }
45
49
 
46
- // Default to light if not recognized
47
50
  return UIBlurEffectStyleLight;
48
51
  }
49
52
 
53
+ #pragma mark - Initialization
54
+
55
+ - (instancetype)init {
56
+ if (self = [super init]) {
57
+ _blurInteraction = YES;
58
+ }
59
+ return self;
60
+ }
61
+
62
+ #pragma mark - Public
63
+
64
+ - (void)applyBlurEffect {
65
+ if (!self.blurTint || self.blurTint.length == 0) {
66
+ [self removeBlurEffect];
67
+ return;
68
+ }
69
+
70
+ // Stop and clear existing animator
71
+ if (_blurAnimator) {
72
+ [_blurAnimator stopAnimation:YES];
73
+ _blurAnimator = nil;
74
+ }
75
+
76
+ // Clear existing effect
77
+ self.effect = nil;
78
+
79
+ UIBlurEffectStyle style = [TrueSheetBlurView blurEffectStyleFromString:self.blurTint];
80
+ UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:style];
81
+
82
+ self.userInteractionEnabled = self.blurInteraction;
83
+
84
+ // Use animator to control blur intensity
85
+ __weak typeof(self) weakSelf = self;
86
+ _blurAnimator = [[UIViewPropertyAnimator alloc] initWithDuration:1.0
87
+ curve:UIViewAnimationCurveLinear
88
+ animations:^{
89
+ weakSelf.effect = blurEffect;
90
+ }];
91
+ _blurAnimator.pausesOnCompletion = YES;
92
+
93
+ // Set intensity: nil means system default (100%), otherwise use provided value (0-100)
94
+ CGFloat intensity =
95
+ (self.blurIntensity && [self.blurIntensity floatValue] >= 0) ? [self.blurIntensity floatValue] / 100.0 : 1.0;
96
+ _blurAnimator.fractionComplete = intensity;
97
+ }
98
+
99
+ - (void)removeBlurEffect {
100
+ if (_blurAnimator) {
101
+ [_blurAnimator stopAnimation:YES];
102
+ _blurAnimator = nil;
103
+ }
104
+
105
+ self.effect = nil;
106
+ }
107
+
108
+ - (void)dealloc {
109
+ [self removeBlurEffect];
110
+ }
111
+
50
112
  @end
@@ -0,0 +1,39 @@
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 TrueSheetDragEvents : NSObject
19
+
20
+ + (void)emitDragBegin:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter
21
+ index:(NSInteger)index
22
+ position:(CGFloat)position
23
+ detent:(CGFloat)detent;
24
+
25
+ + (void)emitDragChange:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter
26
+ index:(NSInteger)index
27
+ position:(CGFloat)position
28
+ detent:(CGFloat)detent;
29
+
30
+ + (void)emitDragEnd:(std::shared_ptr<const facebook::react::EventEmitter>)eventEmitter
31
+ index:(NSInteger)index
32
+ position:(CGFloat)position
33
+ detent:(CGFloat)detent;
34
+
35
+ @end
36
+
37
+ NS_ASSUME_NONNULL_END
38
+
39
+ #endif // RCT_NEW_ARCH_ENABLED