Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### 🐛 Bug fixes

- **iOS**: Fixed liquid glass background showing instead of configured `backgroundColor` on macOS "Designed for iPhone/iPad" compatibility mode. ([#598](https://github.com/lodev09/react-native-true-sheet/pull/598) by [@isaacrowntree](https://github.com/isaacrowntree))
- **iOS**: Added mouse drag gesture support for macOS "Designed for iPhone/iPad" where the native sheet pan gesture doesn't respond to mouse input. ([#598](https://github.com/lodev09/react-native-true-sheet/pull/598) by [@isaacrowntree](https://github.com/isaacrowntree))
- **iOS**: Fixed keyboard scroll positioning when sheet auto-expands from a smaller detent. ([#592](https://github.com/lodev09/react-native-true-sheet/pull/592) by [@lodev09](https://github.com/lodev09))
- **Android**: Fixed dead state after rapid present/dismiss cycles. ([#593](https://github.com/lodev09/react-native-true-sheet/pull/593) by [@lodev09](https://github.com/lodev09))
- **iOS**: Fixed position change not emitting when detent or index changed. ([#584](https://github.com/lodev09/react-native-true-sheet/pull/584) by [@lodev09](https://github.com/lodev09))
Expand Down
129 changes: 127 additions & 2 deletions ios/TrueSheetViewController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ @implementation TrueSheetViewController {
TrueSheetBlurView *_blurView;
TrueSheetGrabberView *_grabberView;
TrueSheetDetentCalculator *_detentCalculator;

// macOS "Designed for iPhone/iPad" mouse drag support
UIPanGestureRecognizer *_macPanGesture;
NSInteger _macDragStartIndex;
}

#pragma mark - Initialization
Expand Down Expand Up @@ -373,6 +377,122 @@ - (void)setupGestureRecognizer {
selector:@selector(handlePanGesture:)];
}
}

[self setupMacPanGesture];
}

#pragma mark - macOS Mouse Drag Support

- (void)setupMacPanGesture {
if (!NSProcessInfo.processInfo.isiOSAppOnMac) return;

UIView *presentedView = self.presentedView;
if (!presentedView || _macPanGesture) return;

_macPanGesture = [[UIPanGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleMacPanGesture:)];
_macPanGesture.minimumNumberOfTouches = 0;
_macPanGesture.maximumNumberOfTouches = 1;

// Add to the presented view — covers the header/grabber area for sheet dragging.
// Deliberately NOT added to the scroll view: the scroll view should handle mouse
// scrolling normally (like native iOS where content scrolls at the largest detent).
[presentedView addGestureRecognizer:_macPanGesture];
}

/**
* Handles mouse drag on macOS "Designed for iPhone/iPad" mode.
*
* Native iOS sheets follow the finger smoothly during drag, then snap to
* the target detent on release based on position + velocity. Since we can
* only snap to detents (no smooth tracking), we defer ALL snapping to
* the release phase:
*
* - During drag: track translation, emit drag events, but don't move the sheet.
* - On release: compute the projected landing position using the drag's
* cumulative translation + velocity (decelerating over ~0.3s). Snap to
* whichever detent that projected position is closest to.
*
* This eliminates mid-drag jitter and gives natural flick-to-expand/collapse.
*/
- (void)handleMacPanGesture:(UIPanGestureRecognizer *)gesture {
if (!self.draggable) return;

NSInteger detentCount = self.detents.count;
if (detentCount == 0) return;

switch (gesture.state) {
case UIGestureRecognizerStateBegan: {
_isDragging = YES;
_macDragStartIndex = _activeDetentIndex;

NSInteger index = self.currentDetentIndex;
CGFloat detent = [self detentValueForIndex:index];
[self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition detent:detent];
break;
}

case UIGestureRecognizerStateChanged: {
NSInteger index = self.currentDetentIndex;
CGFloat detent = [self detentValueForIndex:index];
[self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition detent:detent];
break;
}

case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
CGFloat translationY = [gesture translationInView:gesture.view.window].y;
CGFloat velocityY = [gesture velocityInView:gesture.view.window].y;

// Project where the drag would land: current translation + velocity
// decelerating over a short duration (mimics UIScrollView deceleration).
static const CGFloat kDecelerationDuration = 0.3;
CGFloat projectedTranslation = translationY + velocityY * kDecelerationDuration;

// The sheet's current top-edge Y for the start detent
CGFloat startHeight = [_detentCalculator resolvedHeightForIndex:_macDragStartIndex];
CGFloat screenHeight = self.screenHeight;
CGFloat startTopY = screenHeight - startHeight;

// Projected top-edge Y of the sheet (positive translation = dragged down = higher Y)
CGFloat projectedTopY = startTopY + projectedTranslation;

// Find the detent whose top-edge position is closest to the projected Y
NSInteger targetIndex = _macDragStartIndex;
CGFloat bestDistance = CGFLOAT_MAX;

for (NSInteger i = 0; i < detentCount; i++) {
CGFloat detentHeight = [_detentCalculator resolvedHeightForIndex:i];
CGFloat detentTopY = screenHeight - detentHeight;
CGFloat distance = fabs(projectedTopY - detentTopY);
if (distance < bestDistance) {
bestDistance = distance;
targetIndex = i;
}
}

if (targetIndex != _activeDetentIndex) {
[self.sheet animateChanges:^{
[self resizeToDetentIndex:targetIndex];
}];
}

_isDragging = NO;
NSInteger index = self.currentDetentIndex;
CGFloat detent = [self detentValueForIndex:index];
[self.delegate viewControllerDidDrag:gesture.state index:index position:self.currentPosition detent:detent];

dispatch_async(dispatch_get_main_queue(), ^{
NSInteger settledIndex = self.currentDetentIndex;
[self learnOffsetForDetentIndex:settledIndex];
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"mac drag end"];
});
break;
}
default:
break;
}
}

- (void)setupDraggable {
Expand Down Expand Up @@ -733,15 +853,20 @@ - (void)setupBackground {

#if RNTS_IPHONE_OS_VERSION_AVAILABLE(26_1)
if (@available(iOS 26.1, *)) {
if (!self.isDesignCompatibilityMode) {
if (!self.isDesignCompatibilityMode && [self.sheet respondsToSelector:@selector(setBackgroundEffect:)]) {
if (self.backgroundColor) {
self.sheet.backgroundEffect = [UIColorEffect effectWithColor:self.backgroundColor];
} else if (hasBlur) {
self.sheet.backgroundEffect = [UIColorEffect effectWithColor:[UIColor clearColor]];
} else {
self.sheet.backgroundEffect = nil;
}
return;
// macOS "Designed for iPhone/iPad" compatibility mode does not respect
// sheet.backgroundEffect (UIColorEffect is ignored), so fall through to
// also set view.backgroundColor as a fallback for a solid background.
if (!NSProcessInfo.processInfo.isiOSAppOnMac) {
return;
}
}
}
#endif
Expand Down
6 changes: 6 additions & 0 deletions ios/core/TrueSheetDetentCalculator.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (CGFloat)detentValueForIndex:(NSInteger)index;

/**
Returns the resolved height in points for a given detent index.
Accounts for safe area insets, max content height, and learned offsets.
*/
- (CGFloat)resolvedHeightForIndex:(NSInteger)index;

/**
Learns the offset between resolver height and actual presented height for a detent.
Called when the sheet settles at a detent.
Expand Down
Loading