Skip to content

Commit 7e9b56a

Browse files
committed
feat(ios): bring back position tracking from layout
1 parent 329622b commit 7e9b56a

7 files changed

Lines changed: 82 additions & 42 deletions

File tree

docs/blog/2025/release-3-0.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ import {
9595
ReanimatedTrueSheet,
9696
useReanimatedTrueSheet,
9797
} from '@lodev09/react-native-true-sheet/reanimated'
98-
import Animated, { useAnimatedStyle, interpolate } from 'react-native-reanimated'
98+
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
9999

100100
const App = () => (
101101
<ReanimatedTrueSheetProvider>
@@ -114,7 +114,7 @@ const AnimatedContent = () => {
114114

115115
// Fade in as sheet expands from index 0 to 1
116116
const animatedStyle = useAnimatedStyle(() => ({
117-
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1])
117+
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1], Extrapolation.CLAMP)
118118
}))
119119

120120
return (

docs/docs/guides/reanimated/reanimated.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Use the `useReanimatedTrueSheet` hook to access the sheet's animated values.
6666

6767
```tsx
6868
import { useReanimatedTrueSheet } from '@lodev09/react-native-true-sheet/reanimated'
69-
import Animated, { useAnimatedStyle, interpolate } from 'react-native-reanimated'
69+
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
7070

7171
function MyComponent() {
7272
const { animatedPosition, animatedIndex, animatedDetent } = useReanimatedTrueSheet()
@@ -78,7 +78,7 @@ function MyComponent() {
7878

7979
// Example: Fade in as sheet expands from index 0 to 1
8080
const opacityStyle = useAnimatedStyle(() => ({
81-
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1])
81+
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1], Extrapolation.CLAMP)
8282
}))
8383

8484
return (

docs/docs/migration.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ import {
293293
ReanimatedTrueSheet,
294294
useReanimatedTrueSheet,
295295
} from '@lodev09/react-native-true-sheet/reanimated'
296-
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
296+
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
297297

298298
// 1. Wrap your app with the provider
299299
const App = () => (
@@ -319,7 +319,7 @@ const MyAnimatedComponent = () => {
319319

320320
// Example: Fade in as sheet expands from index 0 to 1
321321
const opacityStyle = useAnimatedStyle(() => ({
322-
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1])
322+
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1], Extrapolation.CLAMP)
323323
}))
324324

325325
return (

ios/TrueSheetView.mm

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask {
223223
[self->_controller setupSheetDetents];
224224
[self->_controller applyActiveDetent];
225225
}];
226-
[_controller updateDraggable];
226+
[_controller setupDraggable];
227227
} else if (_initialDetentIndex >= 0) {
228228
[self presentAtIndex:_initialDetentIndex animated:_initialDetentAnimated completion:nil];
229229
}
@@ -361,7 +361,7 @@ - (void)dismissAnimated:(BOOL)animated completion:(nullable TrueSheetCompletionB
361361
/**
362362
* Debounced sheet update to handle rapid content/header size changes.
363363
*/
364-
- (void)updateSheetIfNeeded {
364+
- (void)setupSheetDetentsForSizeChange {
365365
if (!_controller.isPresented || _isSheetUpdatePending)
366366
return;
367367

@@ -371,19 +371,19 @@ - (void)updateSheetIfNeeded {
371371
self->_isSheetUpdatePending = NO;
372372

373373
[self->_controller.sheetPresentationController animateChanges:^{
374-
[self->_controller setupSheetDetents];
374+
[self->_controller setupSheetDetentsForSizeChange];
375375
}];
376376
});
377377
}
378378

379379
- (void)containerViewContentDidChangeSize:(CGSize)newSize {
380380
_controller.contentHeight = @(newSize.height);
381-
[self updateSheetIfNeeded];
381+
[self setupSheetDetentsForSizeChange];
382382
}
383383

384384
- (void)containerViewHeaderDidChangeSize:(CGSize)newSize {
385385
_controller.headerHeight = @(newSize.height);
386-
[self updateSheetIfNeeded];
386+
[self setupSheetDetentsForSizeChange];
387387
}
388388

389389
#pragma mark - TrueSheetViewControllerDelegate

ios/TrueSheetViewController.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,10 @@ NS_ASSUME_NONNULL_BEGIN
6969
- (void)applyActiveDetent;
7070
- (void)setupActiveDetentWithIndex:(NSInteger)index;
7171
- (void)resizeToDetentIndex:(NSInteger)index;
72-
- (void)setupSheetDetents;
7372
- (void)setupSheetProps;
74-
- (void)updateDraggable;
73+
- (void)setupSheetDetents;
74+
- (void)setupSheetDetentsForSizeChange;
75+
- (void)setupDraggable;
7576

7677
@end
7778

ios/TrueSheetViewController.mm

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ @interface TrueSheetViewController ()
2222

2323
@implementation TrueSheetViewController {
2424
CGFloat _lastPosition;
25-
BOOL _isDragging;
2625
NSInteger _pendingDetentIndex;
26+
BOOL _pendingContentSizeChange;
2727

28-
// Transitioning tracker
28+
// Position tracking
2929
CADisplayLink *_transitioningTimer;
3030
UIView *_transitionFakeView;
31+
BOOL _isDragging;
3132
BOOL _isTransitioning;
33+
BOOL _isTrackingPositionFromLayout;
3234

3335
// Reference to parent TrueSheetViewController (if presented from another sheet)
3436
__weak TrueSheetViewController *_parentSheetController;
@@ -61,10 +63,13 @@ - (instancetype)init {
6163
_lastPosition = 0;
6264
_isDragging = NO;
6365
_isPresented = NO;
66+
_pendingContentSizeChange = NO;
6467
_activeDetentIndex = -1;
6568
_pendingDetentIndex = -1;
69+
6670
_isTransitioning = NO;
6771
_transitionFakeView = [UIView new];
72+
_isTrackingPositionFromLayout = NO;
6873

6974
_blurInteraction = YES;
7075
_resolvedDetentPositions = [NSMutableArray array];
@@ -200,6 +205,9 @@ - (void)viewDidAppear:(BOOL)animated {
200205
if ([self.delegate respondsToSelector:@selector(viewControllerDidFocus)]) {
201206
[self.delegate viewControllerDidFocus];
202207
}
208+
209+
// Emit correct position after presentation
210+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"did present"];
203211
});
204212

205213
[self setupGestureRecognizer];
@@ -264,22 +272,38 @@ - (void)viewDidDisappear:(BOOL)animated {
264272
}
265273
}
266274

275+
- (void)viewWillLayoutSubviews {
276+
[super viewWillLayoutSubviews];
277+
278+
if (!_isTransitioning) {
279+
_isTrackingPositionFromLayout = YES;
280+
281+
UIViewController *presented = self.presentedViewController;
282+
283+
// Not realtime when another controller is presented that triggers our layout
284+
BOOL hasPresentedController = presented != nil && !presented.isBeingPresented && !presented.isBeingDismissed;
285+
286+
BOOL realtime = !hasPresentedController;
287+
288+
if (_pendingContentSizeChange) {
289+
_pendingContentSizeChange = NO;
290+
realtime = NO;
291+
292+
// Store resolved position after content size changes
293+
[self storeResolvedPositionForIndex:self.currentDetentIndex];
294+
}
295+
296+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:realtime debug:@"layout"];
297+
}
298+
}
299+
267300
- (void)viewDidLayoutSubviews {
268301
[super viewDidLayoutSubviews];
269302

270303
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeSize:)]) {
271304
[self.delegate viewControllerDidChangeSize:self.view.frame.size];
272305
}
273306

274-
UIViewController *presented = self.presentedViewController;
275-
BOOL hasPresentedController = presented != nil && !presented.isBeingPresented && !presented.isBeingDismissed;
276-
277-
if (!_isDragging && !_isTransitioning) {
278-
// Update stored position for current detent (handles content size changes)
279-
[self storeResolvedPositionForIndex:self.currentDetentIndex];
280-
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:hasPresentedController];
281-
}
282-
283307
// Emit pending detent change after programmatic resize settles
284308
if (_pendingDetentIndex >= 0) {
285309
NSInteger pendingIndex = _pendingDetentIndex;
@@ -289,9 +313,14 @@ - (void)viewDidLayoutSubviews {
289313
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangeDetent:position:detent:)]) {
290314
CGFloat detent = [self detentValueForIndex:pendingIndex];
291315
[self.delegate viewControllerDidChangeDetent:pendingIndex position:self.currentPosition detent:detent];
316+
317+
// Emit position for the final position
318+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"pending detent change"];
292319
}
293320
});
294321
}
322+
323+
_isTrackingPositionFromLayout = NO;
295324
}
296325

297326
#pragma mark - Presentation Tracking (for RN Screens integration)
@@ -402,7 +431,7 @@ - (void)setupGestureRecognizer {
402431
}
403432
}
404433

405-
- (void)updateDraggable {
434+
- (void)setupDraggable {
406435
UIView *presentedView = self.presentedView;
407436
if (!presentedView)
408437
return;
@@ -432,18 +461,22 @@ - (void)handlePanGesture:(UIPanGestureRecognizer *)gesture {
432461
_isDragging = YES;
433462
break;
434463
case UIGestureRecognizerStateChanged:
435-
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES];
464+
if (!_isTrackingPositionFromLayout) {
465+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:YES debug:@"drag change"];
466+
}
436467
break;
437468
case UIGestureRecognizerStateEnded:
438469
case UIGestureRecognizerStateCancelled: {
439470
if (!_isTransitioning) {
440471
dispatch_async(dispatch_get_main_queue(), ^{
441472
// Store resolved position when drag ends
442473
[self storeResolvedPositionForIndex:self.currentDetentIndex];
443-
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO];
474+
475+
// Emit the correct position after dragging
476+
[self emitChangePositionDelegateWithPosition:self.currentPosition realtime:NO debug:@"drag end"];
444477
});
445478
}
446-
479+
447480
_isDragging = NO;
448481
break;
449482
}
@@ -461,7 +494,7 @@ - (void)setupTransitionTracker {
461494
_isTransitioning = YES;
462495

463496
CGRect dismissedFrame = CGRectMake(0, self.screenHeight, 0, 0);
464-
CGRect presentedFrame = CGRectMake(0, self.presentedView.frame.origin.y, 0, 0);
497+
CGRect presentedFrame = CGRectMake(0, self.currentPosition, 0, 0);
465498

466499
// Set starting fake view position
467500
_transitionFakeView.frame = self.isDismissing ? presentedFrame : dismissedFrame;
@@ -470,7 +503,7 @@ - (void)setupTransitionTracker {
470503
auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
471504
[[context containerView] addSubview:self->_transitionFakeView];
472505

473-
// Set the fake view target position
506+
// Set ending fake view position
474507
self->_transitionFakeView.frame = self.isDismissing ? dismissedFrame : presentedFrame;
475508

476509
self->_transitioningTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleTransitionTracker)];
@@ -495,16 +528,17 @@ - (void)handleTransitionTracker {
495528

496529
if (self.currentPosition >= self.screenHeight) {
497530
// Dismissing position
498-
[self emitChangePositionDelegateWithPosition:layerPosition realtime:YES];
531+
CGFloat position = fmax(_lastPosition, layerPosition);
532+
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition out"];
499533
} else {
500534
// Presenting position
501535
CGFloat position = fmax(self.currentPosition, layerPosition);
502-
[self emitChangePositionDelegateWithPosition:position realtime:YES];
536+
[self emitChangePositionDelegateWithPosition:position realtime:YES debug:@"transition in"];
503537
}
504538
}
505539
}
506540

507-
- (void)emitChangePositionDelegateWithPosition:(CGFloat)position realtime:(BOOL)realtime {
541+
- (void)emitChangePositionDelegateWithPosition:(CGFloat)position realtime:(BOOL)realtime debug:(NSString *)debug {
508542
// Use epsilon comparison to avoid missing updates due to floating point precision
509543
if (fabs(_lastPosition - position) > 0.01) {
510544
_lastPosition = position;
@@ -514,6 +548,9 @@ - (void)emitChangePositionDelegateWithPosition:(CGFloat)position realtime:(BOOL)
514548
if ([self.delegate respondsToSelector:@selector(viewControllerDidChangePosition:position:detent:realtime:)]) {
515549
[self.delegate viewControllerDidChangePosition:index position:position detent:detent realtime:realtime];
516550
}
551+
552+
// Debug position tracking
553+
// NSLog(@"position from %@: %f, realtime: %i", debug, position, realtime);
517554
}
518555
}
519556

@@ -658,6 +695,11 @@ - (CGFloat)detentValueForIndex:(NSInteger)index {
658695

659696
#pragma mark - Sheet Configuration
660697

698+
- (void)setupSheetDetentsForSizeChange {
699+
_pendingContentSizeChange = YES;
700+
[self setupSheetDetents];
701+
}
702+
661703
- (void)setupSheetDetents {
662704
UISheetPresentationController *sheet = self.sheetPresentationController;
663705
if (!sheet)

src/reanimated/ReanimatedTrueSheetProvider.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createContext, useContext, useMemo, type ReactNode } from 'react';
1+
import { createContext, useContext, type ReactNode } from 'react';
22
import { useWindowDimensions } from 'react-native';
33
import { useSharedValue, type SharedValue } from 'react-native-reanimated';
44

@@ -48,14 +48,11 @@ export const ReanimatedTrueSheetProvider = ({ children }: ReanimatedTrueSheetPro
4848
const animatedIndex = useSharedValue(-1);
4949
const animatedDetent = useSharedValue(0);
5050

51-
const value = useMemo(
52-
() => ({
53-
animatedPosition,
54-
animatedIndex,
55-
animatedDetent,
56-
}),
57-
[animatedPosition, animatedIndex, animatedDetent]
58-
);
51+
const value: ReanimatedTrueSheetContextValue = {
52+
animatedPosition,
53+
animatedIndex,
54+
animatedDetent,
55+
}
5956

6057
return (
6158
<ReanimatedTrueSheetContext.Provider value={value}>

0 commit comments

Comments
 (0)