From f83662275dc71c5a1547a6867254a31e40780b24 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 23:47:19 +0000 Subject: [PATCH] Observe content view bounds natively to update sheet height on iOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the async JS bridge round-trip (onLayout → updateLayout()) with native KVO observation on the content view's bounds. When React Native's Yoga layout engine sets the frame, the KVO callback fires synchronously on the same run loop iteration and calls updateLayout() directly. This eliminates the timing issues that surfaced after enabling iOS 26 design. https://claude.ai/code/session_01Rp7ef1h3fKh6fhStcjqLJ5 --- modules/bottom-sheet/ios/SheetView.swift | 27 +++++++++++++++++++ .../src/BottomSheetNativeComponent.tsx | 4 +-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/modules/bottom-sheet/ios/SheetView.swift b/modules/bottom-sheet/ios/SheetView.swift index 360c7a9101..8d9f07da58 100644 --- a/modules/bottom-sheet/ios/SheetView.swift +++ b/modules/bottom-sheet/ios/SheetView.swift @@ -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() @@ -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 @@ -147,6 +152,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate { self.sheetVc = sheetVc self.isOpening = true + self.startObservingContentHeight() rvc.present(sheetVc, animated: true) { [weak self] in self?.isOpening = false @@ -154,6 +160,27 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate { } } + // 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. + 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 } + self.updateLayout() + } + } + 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, diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx index 0fa4c8aa23..788abf9d9d 100644 --- a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx @@ -126,9 +126,9 @@ export class BottomSheetNativeComponent extends React.Component< // a console.log() - just sending the log to the CLI is enough. // TODO: Get to the bottom of this and fix it properly! -sfn setTimeout(() => this.updateLayout()) - } else { - this.updateLayout() } + // iOS: content height changes are observed natively via KVO on + // the content view's bounds, so no JS bridge round-trip needed. }} />