diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5295a534..632ca306 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### 🐛 Bug fixes
+
+- **iOS**: `onPositionChange` no longer emits a slightly fractional `index` during the present transition — the detent offset is now learned before positions are emitted, so the index lands exactly on the target detent instead of snapping after `didPresent`. ([#731](https://github.com/lodev09/react-native-true-sheet/pull/731) by [@lodev09](https://github.com/lodev09))
+
## 3.11.5
### 🎉 New features
diff --git a/example/bare/ios/Podfile.lock b/example/bare/ios/Podfile.lock
index 01a75f4e..e51db5fd 100644
--- a/example/bare/ios/Podfile.lock
+++ b/example/bare/ios/Podfile.lock
@@ -2049,7 +2049,7 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- - RNTrueSheet (3.11.4):
+ - RNTrueSheet (3.11.5):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -2481,7 +2481,7 @@ SPEC CHECKSUMS:
RNGestureHandler: 2ff61eac036eaf89f6818bf4ed9c39771a17d134
RNReanimated: f735b1747a7a93bda7ca102c6d37a3cf54b6d5e8
RNScreens: 991cc417cd396602a6cf59a42139e5a9d91462a9
- RNTrueSheet: 4dcd3292bcfe21502d48e0fd26b4fb3f68da0ad9
+ RNTrueSheet: 4dfa8b05506db77a67c8a2a54bc563338450408c
RNWorklets: 4931990f73bc8f347586918b2e13f11dfadf3b75
Yoga: 77dfa8673de2874e1855002ae59c68b8be9b007b
diff --git a/example/shared/src/components/sheets/BasicSheet.tsx b/example/shared/src/components/sheets/BasicSheet.tsx
index e6c16734..ca84e5aa 100644
--- a/example/shared/src/components/sheets/BasicSheet.tsx
+++ b/example/shared/src/components/sheets/BasicSheet.tsx
@@ -123,9 +123,9 @@ export const BasicSheet = forwardRef((props: BasicSheetProps, ref: Ref
{contentCount > 0 && }
+
diff --git a/ios/TrueSheetViewController.mm b/ios/TrueSheetViewController.mm
index 0a0fbd6a..5ebaf861 100644
--- a/ios/TrueSheetViewController.mm
+++ b/ios/TrueSheetViewController.mm
@@ -261,14 +261,12 @@ - (void)viewDidAppear:(BOOL)animated {
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger index = [self currentDetentIndex];
- [self learnOffsetForDetentIndex:index];
-
CGFloat detent = [self detentValueForIndex:index];
[self.delegate viewControllerDidPresentAtIndex:index position:self.currentPosition detent:detent];
[self.delegate viewControllerDidFocus];
[self->_grabberView updateAccessibilityValueWithIndex:index detentCount:self->_detents.count];
- [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"did present"];
+ [self settleAtDetentIndex:index debug:@"did present"];
});
[self setupGestureRecognizer];
@@ -473,18 +471,17 @@ - (void)viewWillLayoutSubviews {
if (!_isTransitioning && !_isInteractiveDismiss) {
_isTrackingPositionFromLayout = YES;
- UIViewController *presented = self.presentedViewController;
- BOOL hasPresentedController = presented != nil && !presented.isBeingPresented && !presented.isBeingDismissed;
- BOOL realtime = !hasPresentedController;
-
if (_pendingContentSizeChange || _pendingDetentsChange) {
_pendingContentSizeChange = NO;
_pendingDetentsChange = NO;
- realtime = NO;
- [self learnOffsetForDetentIndex:self.currentDetentIndex];
+ [self settleAtDetentIndex:self.currentDetentIndex debug:@"layout"];
+ } else {
+ UIViewController *presented = self.presentedViewController;
+ BOOL hasPresentedController = presented != nil && !presented.isBeingPresented && !presented.isBeingDismissed;
+ [self emitChangePositionDelegateWithPosition:self.currentPosition
+ realtime:!hasPresentedController
+ debug:@"layout"];
}
-
- [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:realtime debug:@"layout"];
}
}
@@ -503,11 +500,10 @@ - (void)viewDidLayoutSubviews {
_pendingDetentIndex = -1;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- [self learnOffsetForDetentIndex:pendingIndex];
CGFloat detent = [self detentValueForIndex:pendingIndex];
[self.delegate viewControllerDidChangeDetent:pendingIndex position:self.currentPosition detent:detent];
[self->_grabberView updateAccessibilityValueWithIndex:pendingIndex detentCount:self->_detents.count];
- [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"pending detent change"];
+ [self settleAtDetentIndex:pendingIndex debug:@"pending detent change"];
});
}
@@ -582,9 +578,8 @@ - (void)handlePanGesture:(UIPanGestureRecognizer *)gesture {
if (!_isTransitioning) {
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger index = self.currentDetentIndex;
- [self learnOffsetForDetentIndex:index];
[self->_grabberView updateAccessibilityValueWithIndex:index detentCount:self->_detents.count];
- [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"drag end"];
+ [self settleAtDetentIndex:index debug:@"drag end"];
});
}
@@ -602,6 +597,13 @@ - (void)setupTransitionTracker {
_isTransitioning = YES;
+ // Learn the resolver-vs-actual offset before emitting transition positions so
+ // the interpolated index lands exactly on the target detent. The presented
+ // frame is already final here (UIKit animates the layer, not the frame).
+ if (!self.isBeingDismissed) {
+ [self learnOffsetForDetentIndex:self.currentDetentIndex];
+ }
+
CGRect dismissedFrame = CGRectMake(0, self.screenHeight, 0, 0);
CGRect presentedFrame = CGRectMake(0, self.currentPosition, 0, 0);
@@ -810,6 +812,16 @@ - (void)learnOffsetForDetentIndex:(NSInteger)index {
[_detentCalculator learnOffsetForDetentIndex:index];
}
+/**
+ * Learn the resolver-vs-actual offset at a settled detent, then emit the corrected
+ * position. Only valid when presentedView's frame is final — never mid-drag or
+ * mid-animation, where learning would store a bogus offset.
+ */
+- (void)settleAtDetentIndex:(NSInteger)index debug:(NSString *)debug {
+ [self learnOffsetForDetentIndex:index];
+ [self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:debug];
+}
+
- (BOOL)findSegmentForPosition:(CGFloat)position outIndex:(NSInteger *)outIndex outProgress:(CGFloat *)outProgress {
return [_detentCalculator findSegmentForPosition:position outIndex:outIndex outProgress:outProgress];
}