@lodev09/react-native-true-sheet 3.0.0-beta.7 → 3.0.0-beta.8

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 (35) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +51 -49
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +10 -18
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +76 -20
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderView.kt +38 -0
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetHeaderViewManager.kt +21 -0
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetPackage.kt +1 -0
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +81 -147
  8. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +303 -409
  9. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +2 -4
  10. package/android/src/main/java/com/lodev09/truesheet/core/RNScreensFragmentObserver.kt +116 -0
  11. package/android/src/main/java/com/lodev09/truesheet/utils/ScreenUtils.kt +33 -5
  12. package/ios/TrueSheetContainerView.h +20 -2
  13. package/ios/TrueSheetContainerView.mm +61 -14
  14. package/ios/TrueSheetContentView.h +4 -2
  15. package/ios/TrueSheetContentView.mm +29 -72
  16. package/ios/TrueSheetFooterView.mm +2 -2
  17. package/ios/TrueSheetHeaderView.h +29 -0
  18. package/ios/TrueSheetHeaderView.mm +60 -0
  19. package/ios/TrueSheetView.mm +178 -232
  20. package/ios/TrueSheetViewController.h +1 -2
  21. package/ios/TrueSheetViewController.mm +126 -236
  22. package/ios/utils/LayoutUtil.h +2 -1
  23. package/ios/utils/LayoutUtil.mm +14 -1
  24. package/lib/module/TrueSheet.js +10 -2
  25. package/lib/module/TrueSheet.js.map +1 -1
  26. package/lib/module/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
  27. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  28. package/lib/typescript/src/TrueSheet.types.d.ts +9 -9
  29. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  30. package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts +6 -0
  31. package/lib/typescript/src/fabric/TrueSheetHeaderViewNativeComponent.d.ts.map +1 -0
  32. package/package.json +4 -1
  33. package/src/TrueSheet.tsx +10 -0
  34. package/src/TrueSheet.types.ts +10 -11
  35. package/src/fabric/TrueSheetHeaderViewNativeComponent.ts +8 -0
@@ -7,13 +7,13 @@
7
7
  //
8
8
 
9
9
  #import "TrueSheetViewController.h"
10
+ #import "TrueSheetContentView.h"
10
11
  #import "utils/ConversionUtil.h"
12
+ #import "utils/GestureUtil.h"
11
13
  #import "utils/WindowUtil.h"
12
14
 
13
15
  #import <React/RCTLog.h>
14
16
  #import <React/RCTScrollViewComponentView.h>
15
- #import "TrueSheetContentView.h"
16
- #import "utils/GestureUtil.h"
17
17
 
18
18
  @interface TrueSheetViewController ()
19
19
 
@@ -25,14 +25,19 @@
25
25
  BOOL _isTransitioning;
26
26
  BOOL _isDragging;
27
27
  BOOL _isTrackingPositionFromLayout;
28
+
29
+ // Hidden view used to track position during native transition animations
28
30
  UIView *_fakeTransitionView;
29
31
  CADisplayLink *_displayLink;
30
32
  }
31
33
 
34
+ #pragma mark - Initialization
35
+
32
36
  - (instancetype)init {
33
37
  if (self = [super initWithNibName:nil bundle:nil]) {
34
38
  _detents = @[ @0.5, @1 ];
35
39
  _contentHeight = @(0);
40
+ _headerHeight = @(0);
36
41
  _grabber = YES;
37
42
  _dimmed = YES;
38
43
  _dimmedDetentIndex = @(0);
@@ -46,7 +51,6 @@
46
51
  _isPresented = NO;
47
52
  _activeDetentIndex = -1;
48
53
 
49
- // Initialize fake transition view for tracking position during animations
50
54
  _fakeTransitionView = [[UIView alloc] init];
51
55
  _fakeTransitionView.hidden = YES;
52
56
  _fakeTransitionView.userInteractionEnabled = NO;
@@ -57,22 +61,18 @@
57
61
  - (void)dealloc {
58
62
  [[NSNotificationCenter defaultCenter] removeObserver:self];
59
63
 
60
- // Ensure display link is invalidated to prevent retain cycle
61
64
  if (_displayLink) {
62
65
  [_displayLink invalidate];
63
66
  _displayLink = nil;
64
67
  }
65
68
  }
66
69
 
67
- #pragma mark - Presentation State
70
+ #pragma mark - Computed Properties
68
71
 
69
72
  - (BOOL)isTopmostPresentedController {
70
- // Check if we're in the window hierarchy and visible
71
73
  if (!self.isViewLoaded || self.view.window == nil) {
72
74
  return NO;
73
75
  }
74
-
75
- // Check if another controller is presented on top of this sheet
76
76
  return self.presentedViewController == nil;
77
77
  }
78
78
 
@@ -84,22 +84,71 @@
84
84
  return self.sheetPresentationController.presentedView;
85
85
  }
86
86
 
87
+ - (CGFloat)currentPosition {
88
+ UIView *presentedView = self.presentedView;
89
+ return presentedView ? presentedView.frame.origin.y : 0.0;
90
+ }
91
+
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;
107
+ }
108
+
109
+ - (NSInteger)currentDetentIndex {
110
+ UISheetPresentationController *sheet = self.sheetPresentationController;
111
+ if (!sheet)
112
+ return -1;
113
+
114
+ UISheetPresentationControllerDetentIdentifier selectedIdentifier = sheet.selectedDetentIdentifier;
115
+ if (!selectedIdentifier)
116
+ return -1;
117
+
118
+ NSArray<UISheetPresentationControllerDetent *> *detents = sheet.detents;
119
+ for (NSInteger i = 0; i < detents.count; i++) {
120
+ if (@available(iOS 16.0, *)) {
121
+ if ([detents[i].identifier isEqualToString:selectedIdentifier]) {
122
+ return i;
123
+ }
124
+ } else {
125
+ // iOS 15 only supports medium/large system detents
126
+ if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierMedium]) {
127
+ return 0;
128
+ } else if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierLarge]) {
129
+ return detents.count - 1;
130
+ }
131
+ }
132
+ }
133
+
134
+ return -1;
135
+ }
136
+
137
+ #pragma mark - View Lifecycle
138
+
87
139
  - (void)viewDidLoad {
88
140
  [super viewDidLoad];
89
-
90
141
  self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
91
142
  }
92
143
 
93
144
  - (void)viewWillAppear:(BOOL)animated {
94
145
  [super viewWillAppear:animated];
95
146
 
96
- // Only trigger willPresent on the initial presentation, not on repositioning
147
+ // Only trigger on initial presentation, not repositioning
97
148
  if (!_isPresented) {
98
149
  if ([self.delegate respondsToSelector:@selector(viewControllerWillPresent)]) {
99
150
  [self.delegate viewControllerWillPresent];
100
151
  }
101
-
102
- // Setup transition position tracking
103
152
  [self setupTransitionPositionTracking];
104
153
  }
105
154
  }
@@ -107,15 +156,11 @@
107
156
  - (void)viewDidAppear:(BOOL)animated {
108
157
  [super viewDidAppear:animated];
109
158
 
110
- // Only trigger didPresent on the initial presentation, not on repositioning
111
159
  if (!_isPresented) {
112
160
  if ([self.delegate respondsToSelector:@selector(viewControllerDidPresent)]) {
113
161
  [self.delegate viewControllerDidPresent];
114
162
  }
115
-
116
- // Setup gesture recognizer after view appears and React content is mounted
117
163
  [self setupGestureRecognizer];
118
-
119
164
  _isPresented = YES;
120
165
  }
121
166
  }
@@ -123,7 +168,6 @@
123
168
  - (void)viewWillDisappear:(BOOL)animated {
124
169
  [super viewWillDisappear:animated];
125
170
 
126
- // Only dispatch willDismiss if the sheet is actually being dismissed
127
171
  if (self.isBeingDismissed && [self.delegate respondsToSelector:@selector(viewControllerWillDismiss)]) {
128
172
  [self.delegate viewControllerWillDismiss];
129
173
  }
@@ -135,8 +179,7 @@
135
179
  - (void)viewDidDisappear:(BOOL)animated {
136
180
  [super viewDidDisappear:animated];
137
181
 
138
- // Only dispatch didDismiss if the sheet is actually being dismissed
139
- // (not when another modal is presented on top)
182
+ // Only dispatch didDismiss when actually dismissing (not when another modal is presented on top)
140
183
  BOOL isActuallyDismissing = self.presentingViewController == nil || self.isBeingDismissed;
141
184
 
142
185
  if (isActuallyDismissing && [self.delegate respondsToSelector:@selector(viewControllerDidDismiss)]) {
@@ -145,49 +188,28 @@
145
188
 
146
189
  _isTrackingPositionFromLayout = NO;
147
190
 
148
- // Only reset state if actually dismissing
149
191
  if (isActuallyDismissing) {
150
192
  _isPresented = NO;
151
193
  _activeDetentIndex = -1;
152
194
  }
153
195
  }
154
196
 
155
- - (void)viewWillTransitionToSize:(CGSize)size
156
- withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
157
- [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
158
-
159
- // Handle rotation/size change
160
- [coordinator
161
- animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
162
- // Animation block - updates happen here
163
- }
164
- completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
165
- // After rotation completes
166
- [self setupSheetDetents];
167
-
168
- // Notify delegate of size change for state update
169
- if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeSize:)]) {
170
- [self.delegate viewControllerDidChangeSize:size];
171
- }
172
- }];
173
- }
174
-
175
197
  - (void)viewDidLayoutSubviews {
176
198
  [super viewDidLayoutSubviews];
177
199
 
200
+ if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeSize:)]) {
201
+ [self.delegate viewControllerDidChangeSize:self.view.frame.size];
202
+ }
203
+
178
204
  if (!_isTransitioning && self.isActiveAndVisible) {
179
- // Flag that we are tracking position from layout
180
205
  _isTrackingPositionFromLayout = YES;
181
206
 
182
- // If another controller is presented on top, treat position changes as transitioning
183
- // This prevents incorrect position notifications when overlays adjust our size
207
+ // Treat position changes as transitioning when another controller is presented on top
184
208
  [self emitChangePositionDelegateWithPosition:self.currentPosition
185
209
  transitioning:_layoutTransitioning || !self.isTopmostPresentedController];
186
210
 
187
- // On IOS 26, this is called twice when we have a ScrollView
188
- // Schedule flag reset after animation to avoid race condition
211
+ // On iOS 26, this is called twice when we have a ScrollView
189
212
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
190
- // Reset layout transitioning after sending notification
191
213
  self->_layoutTransitioning = NO;
192
214
  });
193
215
  }
@@ -195,19 +217,11 @@
195
217
 
196
218
  #pragma mark - Gesture Handling
197
219
 
198
- /**
199
- * Finds the TrueSheetContentView in the hierarchy.
200
- *
201
- * @param view The presentedView to start searching from
202
- * @return The TrueSheetContentView found, or nil
203
- */
204
220
  - (TrueSheetContentView *)findContentView:(UIView *)view {
205
- // Check if this view itself is TrueSheetContentView
206
221
  if ([view isKindOfClass:[TrueSheetContentView class]]) {
207
222
  return (TrueSheetContentView *)view;
208
223
  }
209
224
 
210
- // Recursively search all subviews
211
225
  for (UIView *subview in view.subviews) {
212
226
  TrueSheetContentView *found = [self findContentView:subview];
213
227
  if (found) {
@@ -223,20 +237,17 @@
223
237
  if (!presentedView)
224
238
  return;
225
239
 
226
- // Attach to presented view's pan gestures (sheet's own drag gesture from UIKit)
240
+ // Attach to presented view's pan gesture (sheet's drag gesture from UIKit)
227
241
  [GestureUtil attachPanGestureHandler:presentedView target:self selector:@selector(handlePanGesture:)];
228
242
 
229
- // Find and attach to the first ScrollView's pan gesture in the view hierarchy
230
- // This handles cases where the sheet content includes a ScrollView
243
+ // Also attach to ScrollView's pan gesture if present
231
244
  TrueSheetContentView *contentView = [self findContentView:presentedView];
232
245
  if (contentView) {
233
246
  RCTScrollViewComponentView *scrollViewComponent = [contentView findScrollView];
234
- if (scrollViewComponent) {
235
- // Access the internal UIScrollView via the scrollView property
236
- UIScrollView *scrollView = scrollViewComponent.scrollView;
237
- if (scrollView) {
238
- [GestureUtil attachPanGestureHandler:scrollView target:self selector:@selector(handlePanGesture:)];
239
- }
247
+ if (scrollViewComponent && scrollViewComponent.scrollView) {
248
+ [GestureUtil attachPanGestureHandler:scrollViewComponent.scrollView
249
+ target:self
250
+ selector:@selector(handlePanGesture:)];
240
251
  }
241
252
  }
242
253
  }
@@ -253,14 +264,14 @@
253
264
  _isDragging = YES;
254
265
  break;
255
266
  case UIGestureRecognizerStateChanged:
256
- if (!_isTrackingPositionFromLayout)
267
+ if (!_isTrackingPositionFromLayout) {
257
268
  [self emitChangePositionDelegateWithPosition:self.currentPosition transitioning:NO];
269
+ }
258
270
  break;
259
271
  case UIGestureRecognizerStateEnded:
260
272
  case UIGestureRecognizerStateCancelled:
261
273
  _isDragging = NO;
262
274
  break;
263
-
264
275
  default:
265
276
  break;
266
277
  }
@@ -268,17 +279,11 @@
268
279
 
269
280
  #pragma mark - Position Tracking
270
281
 
271
- /**
272
- * Emits position change to the delegate if the position has changed.
273
- * @param transitioning Whether the position change is part of a transition animation
274
- */
275
282
  - (void)emitChangePositionDelegateWithPosition:(CGFloat)position transitioning:(BOOL)transitioning {
276
283
  if (_lastPosition != position) {
277
284
  _lastPosition = position;
278
285
 
279
- // Emit position change delegate
280
286
  NSInteger index = [self currentDetentIndex];
281
-
282
287
  if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:transitioning:)]) {
283
288
  [self.delegate viewControllerDidChangePosition:index position:position transitioning:transitioning];
284
289
  }
@@ -288,116 +293,79 @@
288
293
  /**
289
294
  * Sets up position tracking during view controller transitions using a fake view.
290
295
  *
291
- * This approach uses a hidden "fake" view added to the container that animates
292
- * alongside the actual presented view. By observing the presentation layer of this
293
- * fake view's frame, we can track smooth position changes during the native transition
294
- * animation without manually animating in JavaScript.
295
- *
296
- * The display link fires at screen refresh rate, allowing us to emit position updates
297
- * that match the native animation curve, providing smooth synchronized updates to JS.
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.
298
299
  */
299
300
  - (void)setupTransitionPositionTracking {
300
- if (self.transitionCoordinator != nil) {
301
- _isTransitioning = YES;
302
-
303
- // Get the container view to add our fake transition view
304
- UIView *containerView = self.sheetPresentationController.containerView;
305
- UIView *presentedView = self.presentedView;
306
-
307
- if (!containerView || !presentedView)
308
- return;
301
+ if (self.transitionCoordinator == nil)
302
+ return;
309
303
 
310
- CGRect frame = presentedView.frame;
304
+ _isTransitioning = YES;
311
305
 
312
- // Determine if presenting or dismissing to set correct start position
313
- BOOL isPresenting = self.isBeingPresented;
306
+ UIView *containerView = self.sheetPresentationController.containerView;
307
+ UIView *presentedView = self.presentedView;
314
308
 
315
- // Set initial position based on transition type:
316
- // - Presenting: Start from bottom (containerHeight) and animate up to detent
317
- // - Dismissing: Start from current position and animate down to bottom
318
- if (isPresenting) {
319
- frame.origin.y = self.containerHeight;
320
- } else {
321
- frame.origin.y = presentedView.frame.origin.y;
322
- }
309
+ if (!containerView || !presentedView)
310
+ return;
323
311
 
324
- // Set fake view's initial frame before transition starts
325
- _fakeTransitionView.frame = frame;
312
+ CGRect frame = presentedView.frame;
313
+ BOOL isPresenting = self.isBeingPresented;
326
314
 
327
- auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
328
- // Add fake view to container so it participates in the transition
329
- [[context containerView] addSubview:self->_fakeTransitionView];
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;
330
318
 
331
- // Animate fake view to the presented view's target position
332
- // UIKit will animate this with the same timing curve as the sheet
333
- CGRect finalFrame = presentedView.frame;
334
- finalFrame.origin.y = presentedView.frame.origin.y;
335
- self->_fakeTransitionView.frame = finalFrame;
319
+ auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
320
+ [[context containerView] addSubview:self->_fakeTransitionView];
336
321
 
337
- // Set our last transition position so we can check if
338
- // fake view's presentation layer has changed
339
- // This value will not change if the sheet is being repositioned
340
- // during a transition after a drag
341
- self->_lastTransitionPosition = finalFrame.origin.y;
322
+ CGRect finalFrame = presentedView.frame;
323
+ finalFrame.origin.y = presentedView.frame.origin.y;
324
+ self->_fakeTransitionView.frame = finalFrame;
342
325
 
343
- // Start display link to track position changes at screen refresh rate
344
- // This fires at 60-120Hz and reads from the presentation layer
345
- self->_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(trackTransitionPosition:)];
346
- [self->_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
347
- };
326
+ self->_lastTransitionPosition = finalFrame.origin.y;
348
327
 
349
- [self.transitionCoordinator
350
- animateAlongsideTransition:animation
351
- completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
352
- // Clean up display link
353
- [self->_displayLink invalidate];
354
- self->_displayLink = nil;
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
+ };
355
332
 
356
- // Remove fake view from hierarchy
357
- [self->_fakeTransitionView removeFromSuperview];
358
- self->_isTransitioning = NO;
359
- }];
360
- }
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
+ }];
361
341
  }
362
342
 
363
343
  - (void)trackTransitionPosition:(CADisplayLink *)displayLink {
364
344
  UIView *presentedView = self.presentedView;
365
345
 
366
- if (_isDragging || !_fakeTransitionView || !presentedView) {
346
+ if (_isDragging || !_fakeTransitionView || !presentedView)
367
347
  return;
368
- }
369
348
 
370
- // Get the presentation layer which contains the in-flight animated values
371
- // Unlike the model layer (which has the final/target value), the presentation
372
- // layer reflects the current state during animation
349
+ // Presentation layer contains in-flight animated values (not final/target values)
373
350
  CALayer *presentationLayer = _fakeTransitionView.layer.presentationLayer;
374
351
 
375
352
  if (presentationLayer) {
376
353
  BOOL transitioning = NO;
377
354
  CGFloat position = presentationLayer.frame.origin.y;
378
355
 
379
- // Our last transition position is nearly the same as fake view's layer position
380
- // Sheet must've been repositioning after dragging at lowest detent
381
- // Use 0.5 points as epsilon to account for floating-point precision in layout calculations
356
+ // If position matches last transition position (within epsilon), sheet is repositioning after drag
382
357
  if (fabs(_lastTransitionPosition - position) < 0.5) {
383
- // Let's just flag it as transitioning to let JS manually animate
384
358
  transitioning = YES;
385
-
386
- // Use the target presented view's frame position to animate
387
359
  position = presentedView.frame.origin.y;
388
360
  } else {
389
- // We are actually getting changes to our fake view's layer position
390
- // Just update our last transition position
391
361
  _lastTransitionPosition = position;
392
362
  }
393
363
 
394
- // Emit the current animated Y position to JS
395
- // This provides smooth position updates that match the native animation curve
396
364
  [self emitChangePositionDelegateWithPosition:position transitioning:transitioning];
397
365
  }
398
366
  }
399
367
 
400
- #pragma mark - Sheet Configuration (iOS 15+)
368
+ #pragma mark - Sheet Configuration
401
369
 
402
370
  - (void)setupSheetDetents {
403
371
  UISheetPresentationController *sheet = self.sheetPresentationController;
@@ -406,9 +374,8 @@
406
374
 
407
375
  NSMutableArray<UISheetPresentationControllerDetent *> *detents = [NSMutableArray array];
408
376
 
409
- // Subtract bottomInset from content height to account for safe area
410
- // This prevents iOS from adding extra bottom insets automatically
411
- CGFloat totalHeight = [self.contentHeight floatValue] - self.bottomInset;
377
+ // Subtract bottomInset to prevent iOS from adding extra bottom insets
378
+ CGFloat totalHeight = [self.contentHeight floatValue] + [self.headerHeight floatValue] - self.bottomInset;
412
379
 
413
380
  for (NSInteger index = 0; index < self.detents.count; index++) {
414
381
  id detent = self.detents[index];
@@ -448,7 +415,7 @@
448
415
 
449
416
  CGFloat value = [detent floatValue];
450
417
 
451
- // -1 represents "auto"
418
+ // -1 represents "auto" (fit content height)
452
419
  if (value == -1) {
453
420
  if (@available(iOS 16.0, *)) {
454
421
  NSString *detentId = @"custom-auto";
@@ -458,38 +425,32 @@
458
425
  CGFloat maxDetentValue = context.maximumDetentValue;
459
426
  CGFloat maxValue =
460
427
  self.maxHeight ? fmin(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
461
- CGFloat resolvedValue = fmin(height, maxValue);
462
- return resolvedValue;
428
+ return fmin(height, maxValue);
463
429
  }];
464
430
  } else {
465
431
  return [UISheetPresentationControllerDetent mediumDetent];
466
432
  }
467
433
  }
468
434
 
469
- // Handle fraction (0-1)
470
- // Fraction should only be > 0 and <= 1
471
435
  if (value <= 0 || value > 1) {
472
436
  RCTLogError(@"TrueSheet: detent fraction (%f) must be between 0 and 1", value);
473
437
  return [UISheetPresentationControllerDetent mediumDetent];
474
438
  }
475
439
 
476
- NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
477
-
478
440
  if (@available(iOS 16.0, *)) {
479
- // Use exact comparison for common values
480
441
  if (value == 1.0) {
481
442
  return [UISheetPresentationControllerDetent largeDetent];
482
443
  } else if (value == 0.5) {
483
444
  return [UISheetPresentationControllerDetent mediumDetent];
484
445
  } else {
446
+ NSString *detentId = [NSString stringWithFormat:@"custom-%f", value];
485
447
  return [UISheetPresentationControllerDetent
486
448
  customDetentWithIdentifier:detentId
487
449
  resolver:^CGFloat(id<UISheetPresentationControllerDetentResolutionContext> context) {
488
450
  CGFloat maxDetentValue = context.maximumDetentValue;
489
451
  CGFloat maxValue =
490
452
  self.maxHeight ? fmin(maxDetentValue, [self.maxHeight floatValue]) : maxDetentValue;
491
- CGFloat resolvedValue = fmin(value * maxDetentValue, maxValue);
492
- return resolvedValue;
453
+ return fmin(value * maxDetentValue, maxValue);
493
454
  }];
494
455
  }
495
456
  } else if (value >= 0.5) {
@@ -524,11 +485,9 @@
524
485
  if (!sheet)
525
486
  return;
526
487
 
527
- // Validate and clamp activeDetentIndex to detents bounds
528
488
  NSInteger detentCount = _detents.count;
529
- if (detentCount == 0) {
489
+ if (detentCount == 0)
530
490
  return;
531
- }
532
491
 
533
492
  // Clamp index to valid range
534
493
  NSInteger clampedIndex = _activeDetentIndex;
@@ -538,7 +497,6 @@
538
497
  clampedIndex = detentCount - 1;
539
498
  }
540
499
 
541
- // Update the stored index if it was clamped
542
500
  if (clampedIndex != _activeDetentIndex) {
543
501
  _activeDetentIndex = clampedIndex;
544
502
  }
@@ -554,83 +512,23 @@
554
512
  [self applyActiveDetent];
555
513
  }
556
514
 
557
- - (NSInteger)currentDetentIndex {
558
- UISheetPresentationController *sheet = self.sheetPresentationController;
559
- if (!sheet)
560
- return -1;
561
-
562
- UISheetPresentationControllerDetentIdentifier selectedIdentifier = sheet.selectedDetentIdentifier;
563
- if (!selectedIdentifier)
564
- return -1;
565
-
566
- // Find the index by matching the identifier in the detents array
567
- NSArray<UISheetPresentationControllerDetent *> *detents = sheet.detents;
568
- for (NSInteger i = 0; i < detents.count; i++) {
569
- if (@available(iOS 16.0, *)) {
570
- if ([detents[i].identifier isEqualToString:selectedIdentifier]) {
571
- return i;
572
- }
573
- } else {
574
- // For iOS 15, we only support system detents (medium/large)
575
- // Return the index based on the selected identifier
576
- if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierMedium]) {
577
- return 0;
578
- } else if ([selectedIdentifier isEqualToString:UISheetPresentationControllerDetentIdentifierLarge]) {
579
- return detents.count - 1;
580
- }
581
- }
582
- }
583
-
584
- return -1;
585
- }
586
-
587
- - (CGFloat)currentPosition {
588
- UIView *presentedView = self.presentedView;
589
- if (!presentedView)
590
- return 0.0;
591
-
592
- return presentedView.frame.origin.y;
593
- }
594
-
595
- - (CGFloat)bottomInset {
596
- // No bottom inset for iPad
597
- if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
598
- return 0;
599
- }
600
-
601
- // Get bottom safe area inset from the window's safe area
602
- UIWindow *window = [WindowUtil keyWindow];
603
- return window ? window.safeAreaInsets.bottom : 0;
604
- }
605
-
606
- - (CGFloat)currentHeight {
607
- return self.containerHeight - self.currentPosition - self.bottomInset;
608
- }
609
-
610
- - (CGFloat)containerHeight {
611
- UIView *containerView = self.sheetPresentationController.containerView;
612
- if (!containerView)
613
- return 0.0;
614
-
615
- return containerView.frame.size.height;
616
- }
617
-
618
515
  - (void)setupSheetProps {
619
516
  UISheetPresentationController *sheet = self.sheetPresentationController;
620
517
  if (!sheet) {
518
+ RCTLogWarn(
519
+ @"TrueSheet: No sheet presentation controller available. Ensure the view controller is presented modally.");
621
520
  return;
622
521
  }
623
522
 
624
523
  sheet.delegate = self;
625
524
 
626
- // Configure page sizing behavior (iOS 17+)
627
525
  if (@available(iOS 17.0, *)) {
628
526
  sheet.prefersPageSizing = self.pageSizing;
629
527
  }
630
528
 
631
529
  sheet.prefersEdgeAttachedInCompactHeight = YES;
632
530
  sheet.prefersGrabberVisible = self.grabber;
633
- // Only set preferredCornerRadius if explicitly provided, otherwise use system default
531
+
634
532
  if (self.cornerRadius) {
635
533
  sheet.preferredCornerRadius = [self.cornerRadius floatValue];
636
534
  } else {
@@ -639,19 +537,14 @@
639
537
 
640
538
  self.view.backgroundColor = self.backgroundColor;
641
539
 
642
- // Setup blur effect if blurTint is provided
540
+ // Setup or remove blur effect
643
541
  if (self.blurTint && self.blurTint.length > 0) {
644
542
  UIBlurEffectStyle style = [ConversionUtil blurEffectStyleFromString:self.blurTint];
645
-
646
- // Create a blur effect view and set it as the background
647
543
  UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:style]];
648
544
  blurView.frame = self.view.bounds;
649
545
  blurView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
650
-
651
- // Insert blur view at the bottom
652
546
  [self.view insertSubview:blurView atIndex:0];
653
547
  } else {
654
- // Remove any blur views
655
548
  for (UIView *subview in self.view.subviews) {
656
549
  if ([subview isKindOfClass:[UIVisualEffectView class]]) {
657
550
  [subview removeFromSuperview];
@@ -674,15 +567,12 @@
674
567
 
675
568
  #if RNS_DISMISSIBLE_MODAL_PROTOCOL_AVAILABLE
676
569
  - (BOOL)isDismissible {
677
- // Return NO to prevent react-native-screens from dismissing this sheet
678
- // when presenting a React Navigation modal
570
+ // Prevent react-native-screens from dismissing this sheet when presenting a modal
679
571
  return NO;
680
572
  }
681
573
 
682
574
  - (UIViewController *)newPresentingViewController {
683
- // Return the content view controller as the presenting controller
684
- // This allows react-native-screens to present modals on top of the sheet's content
685
- // instead of trying to present on top of the sheet itself
575
+ // Allow react-native-screens to present modals on top of the sheet's content
686
576
  return self;
687
577
  }
688
578
  #endif
@@ -43,8 +43,9 @@ NS_ASSUME_NONNULL_BEGIN
43
43
  /**
44
44
  * Unpins a view by removing its constraints and re-enabling autoresizing mask translation
45
45
  * @param view The view to unpin
46
+ * @param parentView The parent view that holds the constraints (optional, will use superview if nil)
46
47
  */
47
- + (void)unpinView:(UIView *)view;
48
+ + (void)unpinView:(UIView *)view fromParentView:(nullable UIView *)parentView;
48
49
 
49
50
  @end
50
51