The Great Unjanking of the Sheets (#9973)

This commit is contained in:
Samuel Newman
2026-03-09 22:53:32 +02:00
committed by GitHub
parent 18d7e775f6
commit aa897f55a0
27 changed files with 318 additions and 196 deletions
@@ -19,8 +19,8 @@ public class BottomSheetModule: Module {
view.dismiss()
}
AsyncFunction("updateLayout") { (view: SheetView) in
view.updateLayout()
Prop("fullHeight") { (view: SheetView, prop: Bool) in
view.fullHeight = prop
}
Prop("cornerRadius") { (view: SheetView, prop: Float) in
+32 -9
View File
@@ -8,6 +8,9 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
private var innerView: UIView?
private var touchHandler: RCTTouchHandler?
// Native content height observation (eliminates JS bridge round-trip)
private var contentHeightObservation: NSKeyValueObservation?
// Events
private let onAttemptDismiss = EventDispatcher()
private let onSnapPointChange = EventDispatcher()
@@ -23,6 +26,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
}
// React view props
var fullHeight = false
var preventDismiss = false
var preventExpansion = false
var cornerRadius: CGFloat?
@@ -68,7 +72,6 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
}
}
}
private var prevLayoutDetentIdentifier: UISheetPresentationController.Detent.Identifier?
// MARK: - Lifecycle
@@ -106,6 +109,8 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
}
private func destroy() {
self.contentHeightObservation?.invalidate()
self.contentHeightObservation = nil
self.isClosing = false
self.isOpen = false
self.sheetVc = nil
@@ -128,7 +133,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
}
let sheetVc = SheetViewController()
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion)
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion, fullHeight: self.fullHeight)
if let sheet = sheetVc.sheetPresentationController {
sheet.delegate = self
sheet.preferredCornerRadius = self.cornerRadius
@@ -147,6 +152,9 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
self.sheetVc = sheetVc
self.isOpening = true
if !self.fullHeight {
self.startObservingContentHeight()
}
rvc.present(sheetVc, animated: true) { [weak self] in
self?.isOpening = false
@@ -154,15 +162,30 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
}
}
func updateLayout() {
// Allow updates either when identifiers match OR when prevLayoutDetentIdentifier is nil (first real content update)
if self.prevLayoutDetentIdentifier == self.selectedDetentIdentifier || self.prevLayoutDetentIdentifier == nil,
let contentHeight = self.innerView?.subviews.first?.frame.size.height {
self.sheetVc?.updateDetents(contentHeight: self.clampHeight(contentHeight),
preventExpansion: self.preventExpansion)
// Observe the content view's bounds via KVO so that height changes are detected
// purely on the native side, without a JS bridge round-trip through onLayout.
// Calls updateDetents directly with the observed height rather than going through
// updateLayout(), which has a prevLayoutDetentIdentifier guard that can block
// legitimate content-driven updates when detent identifiers drift during animations.
private func startObservingContentHeight() {
self.contentHeightObservation?.invalidate()
guard let contentView = self.innerView?.subviews.first else { return }
self.contentHeightObservation = contentView.observe(
\.bounds,
options: [.old, .new]
) { [weak self] _, change in
guard let self = self,
(self.isOpen || self.isOpening) && !self.isClosing,
let oldBounds = change.oldValue,
let newBounds = change.newValue,
oldBounds.height != newBounds.height,
newBounds.height > 0 else { return }
let clampedHeight = self.clampHeight(newBounds.height)
self.sheetVc?.updateDetents(contentHeight: clampedHeight, preventExpansion: self.preventExpansion)
self.selectedDetentIdentifier = self.sheetVc?.getCurrentDetentIdentifier()
}
self.prevLayoutDetentIdentifier = self.selectedDetentIdentifier
}
func dismiss() {
@@ -20,13 +20,19 @@ class SheetViewController: UIViewController {
}
}
func setDetents(contentHeight: CGFloat, preventExpansion: Bool) {
func setDetents(contentHeight: CGFloat, preventExpansion: Bool, fullHeight: Bool = false) {
guard let sheet = self.sheetPresentationController,
let screenHeight = Util.getScreenHeight()
else {
return
}
if fullHeight {
sheet.detents = [.large()]
sheet.selectedDetentIdentifier = .large
return
}
// On iOS 26, the floaty sheet presentation adds the device bottom safe area
// on top of the custom detent value, creating visible padding inside the pill.
// Subtract it so the pill height matches our actual content.