Before submitting a new issue
Bug Summary
Two related iOS bugs, both cases of internal detent state not being updated by one of the two mutation paths (programmatic vs. user drag). Found while auditing a production sheet against the 3.11.9 source; exact line references and suggested fixes in Additional Context.
Bug 1 — poisoned offset learning. When a content/footer size change triggers a layout settle while the keyboard has the sheet grown, learnOffsetForDetentIndex: stores an offset inflated by ~keyboardHeight. Afterward, a keyboard-less drag to the top detent reports animatedIndex peaking around 0.4 instead of 1 (animatedDetent misreports equally; raw position stays correct). Expected: learned offsets reflect the resting sheet geometry, and animatedIndex reaches the dragged detent's index.
Bug 2 — resize() no-ops after a user drag. _activeDetentIndex is only written by programmatic paths; user drags never update it, and resizeToDetentIndex: early-returns when the target equals the stale value. Present at index 0 → drag to the top detent → call resize(0): expected the sheet to collapse; instead nothing happens — no motion, no event.
Affected Platforms
Library Version
3.11.9
Environment Info
System:
OS: macOS 26.5.2
CPU: (14) arm64 Apple M4 Pro
Memory: 150.63 MB / 24.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 22.17.1
path: /opt/homebrew/opt/node@22/bin/node
Yarn: Not Found
npm:
version: 10.9.2
path: /opt/homebrew/opt/node@22/bin/npm
Watchman: Not Found
Managers:
CocoaPods:
version: 1.16.2
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 25.5
- iOS 26.5
- macOS 26.5
- tvOS 26.5
- visionOS 26.5
- watchOS 26.5
Android SDK: Not Found
IDEs:
Android Studio: Not Found
Xcode:
version: 26.6/17F113
path: /usr/bin/xcodebuild
Languages:
Java:
version: 24.0.2
path: /usr/bin/javac
Ruby:
version: 2.6.10
path: /usr/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 19.2.3
wanted: 19.2.3
react-native:
installed: 0.86.0
wanted: 0.86.0
react-native-macos: Not Found
# Expo SDK 57 project (CNG), New Architecture + Hermes enabled —
# the CLI can't detect those from CNG config, hence "Not found" above.
Steps to Reproduce
Bug 2 — reproduces in this repo's own example app, unmodified:
- Launch the example, tap TrueSheet View (presents
BasicSheet at index 0 → internal _activeDetentIndex = 0).
- Drag the sheet to the top detent by the grabber (no programmatic resize in between).
- Tap the Peek button (
resize(0)).
- Nothing happens — expected the sheet to collapse to
peek. (Tapping Auto first works, because 1 !== 0; after that, Peek works again — consistent with the stale-index early return.)
Bug 1 — sheet shape: detents={['peek', 0.45]}, dismissible={false}, footer containing a TextInput plus a conditionally rendered bar (footer height changes at runtime), ReanimatedTrueSheet:
- Focus the footer input (keyboard up — UIKit grows the sheet under the keyboard).
- While the keyboard is up, trigger the footer/content resize (show the conditional bar). The resulting layout settle learns a keyboard-inflated offset for the current detent.
- Dismiss the keyboard, collapse to
peek, then drag fully up by the grabber.
animatedIndex peaks ≈ 0.4 instead of 1 — any interpolate(animatedIndex, [0, 1], …) opacity stays ~60% hidden at the top detent. Self-heals only once a clean settle re-learns that index.
Repro
https://github.com/lodev09/react-native-true-sheet/tree/main/example
Bug 2 reproduces in the unmodified example app per the steps above. A Snack isn't possible — the library isn't Expo Go compatible. Bug 1 needs a keyboard + runtime footer resize, which no stock example sheet has; its mechanism is fully traceable in source (see Additional Context), and I'm happy to push a minimal repro repo if needed.
Additional Context
Line references are from the published 3.11.9 package.
Bug 1 mechanism
TrueSheetDetentCalculator.learnOffsetForDetentIndex: stores actualHeight − resolverHeight on every settle ("Always update — system offset can change between detent transitions", TrueSheetDetentCalculator.mm:46), where actualHeight = screenHeight − currentPosition.
One settle path is layout-driven: viewWillLayoutSubviews → settleAtDetentIndex: when _pendingContentSizeChange is set (TrueSheetViewController.mm:477) — i.e. a footer/content resize triggers learning, with no keyboard guard. With the keyboard up, UIKit grows the sheet (bottom edge stays under the keyboard, top edge rises), so actualHeight includes the keyboard growth and the stored offset absorbs ~keyboardHeight.
From then on resolvedHeightForIndex: returns resolverHeight + poisonedOffset, so interpolatedIndexForPosition: / interpolatedDetentForPosition: treat the detent as ~keyboardHeight taller than reality. The fallback in offsetForIndex: ("Fall back to any known offset", lines 77–82) propagates a poisoned offset to detents that never learned one. Both animatedIndex and animatedDetent in the reanimated integration ride this math (same onPositionChange payload); only raw position is immune.
Suggested fix: skip (or defer) offset learning while the keyboard has the sheet grown — the keyboard observer already knows. The v4 branch appears to address this (#744: "Offset learning is flag-driven, so keyboard/rotation moves no longer risk corrupting learned detent offsets"; #756 keeps learning at explicit guarded settles) — so this is mainly (a) a v3-line patch candidate and (b) a concrete keyboard regression case to test the v4 rework against.
Bug 2 mechanism
_activeDetentIndex is only written by programmatic paths — setupActiveDetentWithIndex: (present), resizeToDetentIndex:, and the grabber tap/accessibility cycles. The UISheetPresentationControllerDelegate callback (sheetPresentationControllerDidChangeSelectedDetentIdentifier, TrueSheetViewController.mm:1220) emits the detent-change event but does not sync it. resizeToDetentIndex: early-returns when index == _activeDetentIndex (TrueSheetViewController.mm:1019), hence the silent no-op after a drag.
Adjacent risk from the same stale field: applySheetPropsUpdate (TrueSheetView.mm:782) calls applyActiveDetent inside animateChanges on any prop update while presented, re-asserting selectedDetentIdentifier from the stale index — a prop update after a drag-expansion can snap the sheet back to the old detent.
Suggested fix: sync _activeDetentIndex in the sheet-delegate detent-change callback (and/or the drag-end settle), so the early return compares against the sheet's real detent.
Workaround we ship: on onDetentChange/onDragEnd, call resize(e.nativeEvent.index) — when the sheet is already at that detent this only updates the internal index, so it's a motionless sync.
Happy to test patches for either — our sheet exercises both paths heavily (always-presented peek sheet, keyboard-driven footer resizes, drag + programmatic transitions).
Before submitting a new issue
Bug Summary
Two related iOS bugs, both cases of internal detent state not being updated by one of the two mutation paths (programmatic vs. user drag). Found while auditing a production sheet against the 3.11.9 source; exact line references and suggested fixes in Additional Context.
Bug 1 — poisoned offset learning. When a content/footer size change triggers a layout settle while the keyboard has the sheet grown,
learnOffsetForDetentIndex:stores an offset inflated by ~keyboardHeight. Afterward, a keyboard-less drag to the top detent reportsanimatedIndexpeaking around 0.4 instead of 1 (animatedDetentmisreports equally; rawpositionstays correct). Expected: learned offsets reflect the resting sheet geometry, andanimatedIndexreaches the dragged detent's index.Bug 2 —
resize()no-ops after a user drag._activeDetentIndexis only written by programmatic paths; user drags never update it, andresizeToDetentIndex:early-returns when the target equals the stale value. Present at index 0 → drag to the top detent → callresize(0): expected the sheet to collapse; instead nothing happens — no motion, no event.Affected Platforms
Library Version
3.11.9
Environment Info
System: OS: macOS 26.5.2 CPU: (14) arm64 Apple M4 Pro Memory: 150.63 MB / 24.00 GB Shell: version: "5.9" path: /bin/zsh Binaries: Node: version: 22.17.1 path: /opt/homebrew/opt/node@22/bin/node Yarn: Not Found npm: version: 10.9.2 path: /opt/homebrew/opt/node@22/bin/npm Watchman: Not Found Managers: CocoaPods: version: 1.16.2 path: /opt/homebrew/bin/pod SDKs: iOS SDK: Platforms: - DriverKit 25.5 - iOS 26.5 - macOS 26.5 - tvOS 26.5 - visionOS 26.5 - watchOS 26.5 Android SDK: Not Found IDEs: Android Studio: Not Found Xcode: version: 26.6/17F113 path: /usr/bin/xcodebuild Languages: Java: version: 24.0.2 path: /usr/bin/javac Ruby: version: 2.6.10 path: /usr/bin/ruby npmPackages: "@react-native-community/cli": Not Found react: installed: 19.2.3 wanted: 19.2.3 react-native: installed: 0.86.0 wanted: 0.86.0 react-native-macos: Not Found # Expo SDK 57 project (CNG), New Architecture + Hermes enabled — # the CLI can't detect those from CNG config, hence "Not found" above.Steps to Reproduce
Bug 2 — reproduces in this repo's own example app, unmodified:
BasicSheetat index 0 → internal_activeDetentIndex = 0).resize(0)).peek. (Tapping Auto first works, because1 !== 0; after that, Peek works again — consistent with the stale-index early return.)Bug 1 — sheet shape:
detents={['peek', 0.45]},dismissible={false}, footer containing aTextInputplus a conditionally rendered bar (footer height changes at runtime),ReanimatedTrueSheet:peek, then drag fully up by the grabber.animatedIndexpeaks ≈ 0.4 instead of 1 — anyinterpolate(animatedIndex, [0, 1], …)opacity stays ~60% hidden at the top detent. Self-heals only once a clean settle re-learns that index.Repro
https://github.com/lodev09/react-native-true-sheet/tree/main/example
Bug 2 reproduces in the unmodified example app per the steps above. A Snack isn't possible — the library isn't Expo Go compatible. Bug 1 needs a keyboard + runtime footer resize, which no stock example sheet has; its mechanism is fully traceable in source (see Additional Context), and I'm happy to push a minimal repro repo if needed.
Additional Context
Line references are from the published 3.11.9 package.
Bug 1 mechanism
TrueSheetDetentCalculator.learnOffsetForDetentIndex:storesactualHeight − resolverHeighton every settle ("Always update — system offset can change between detent transitions",TrueSheetDetentCalculator.mm:46), whereactualHeight = screenHeight − currentPosition.One settle path is layout-driven:
viewWillLayoutSubviews→settleAtDetentIndex:when_pendingContentSizeChangeis set (TrueSheetViewController.mm:477) — i.e. a footer/content resize triggers learning, with no keyboard guard. With the keyboard up, UIKit grows the sheet (bottom edge stays under the keyboard, top edge rises), soactualHeightincludes the keyboard growth and the stored offset absorbs ~keyboardHeight.From then on
resolvedHeightForIndex:returnsresolverHeight + poisonedOffset, sointerpolatedIndexForPosition:/interpolatedDetentForPosition:treat the detent as ~keyboardHeight taller than reality. The fallback inoffsetForIndex:("Fall back to any known offset", lines 77–82) propagates a poisoned offset to detents that never learned one. BothanimatedIndexandanimatedDetentin the reanimated integration ride this math (sameonPositionChangepayload); only rawpositionis immune.Suggested fix: skip (or defer) offset learning while the keyboard has the sheet grown — the keyboard observer already knows. The v4 branch appears to address this (#744: "Offset learning is flag-driven, so keyboard/rotation moves no longer risk corrupting learned detent offsets"; #756 keeps learning at explicit guarded settles) — so this is mainly (a) a v3-line patch candidate and (b) a concrete keyboard regression case to test the v4 rework against.
Bug 2 mechanism
_activeDetentIndexis only written by programmatic paths —setupActiveDetentWithIndex:(present),resizeToDetentIndex:, and the grabber tap/accessibility cycles. TheUISheetPresentationControllerDelegatecallback (sheetPresentationControllerDidChangeSelectedDetentIdentifier,TrueSheetViewController.mm:1220) emits the detent-change event but does not sync it.resizeToDetentIndex:early-returns whenindex == _activeDetentIndex(TrueSheetViewController.mm:1019), hence the silent no-op after a drag.Adjacent risk from the same stale field:
applySheetPropsUpdate(TrueSheetView.mm:782) callsapplyActiveDetentinsideanimateChangeson any prop update while presented, re-assertingselectedDetentIdentifierfrom the stale index — a prop update after a drag-expansion can snap the sheet back to the old detent.Suggested fix: sync
_activeDetentIndexin the sheet-delegate detent-change callback (and/or the drag-end settle), so the early return compares against the sheet's real detent.Workaround we ship: on
onDetentChange/onDragEnd, callresize(e.nativeEvent.index)— when the sheet is already at that detent this only updates the internal index, so it's a motionless sync.Happy to test patches for either — our sheet exercises both paths heavily (always-presented peek sheet, keyboard-driven footer resizes, drag + programmatic transitions).