replace react-nav header hack with scroll edge interaction module

- new expo-scroll-edge-interaction native module (iOS-only) that sets up
  UIScrollEdgeElementContainerInteraction on iOS 26+
- new ScrollEdgeInteractionProvider context and hooks
- Layout.Screen gains `transparentHeader` prop
- Layout.Header.Outer auto-detects transparent mode from context
- List auto-registers scroll view and applies content inset

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-02-27 18:13:29 +02:00
parent b7283d92bf
commit 9836939728
15 changed files with 373 additions and 88 deletions
@@ -0,0 +1,73 @@
import ExpoModulesCore
import React
class ExpoScrollEdgeInteractionView: ExpoView {
var nodeHandle: Int? {
didSet {
setupInteraction()
}
}
var scrollViewTag: Int? {
didSet {
setupInteraction()
}
}
var edge: String? {
didSet {
setupInteraction()
}
}
private var currentInteraction: NSObject?
private func setupInteraction() {
guard #available(iOS 26.0, *) else { return }
// Clean up old interaction
if let interaction = currentInteraction as? UIScrollEdgeElementContainerInteraction,
let headerView = findHeaderView() {
headerView.removeInteraction(interaction)
currentInteraction = nil
}
guard let nodeHandle = nodeHandle,
let scrollViewTag = scrollViewTag
else { return }
guard let headerView = appContext?.findView(withTag: nodeHandle, ofType: UIView.self) else {
return
}
// RCTScrollView wraps a UIScrollView — get the inner one
let scrollView: UIScrollView?
if let rctScrollView = appContext?.findView(withTag: scrollViewTag, ofType: RCTScrollView.self) {
scrollView = rctScrollView.scrollView
} else {
scrollView = appContext?.findView(withTag: scrollViewTag, ofType: UIScrollView.self)
}
guard let scrollView = scrollView else { return }
let interaction = UIScrollEdgeElementContainerInteraction()
interaction.edge = resolveEdge()
interaction.scrollView = scrollView
headerView.addInteraction(interaction)
currentInteraction = interaction
}
private func resolveEdge() -> NSDirectionalRectEdge {
switch edge {
case "bottom": return .bottom
case "left": return .leading
case "right": return .trailing
default: return .top
}
}
private func findHeaderView() -> UIView? {
guard let nodeHandle = nodeHandle else { return nil }
return appContext?.findView(withTag: nodeHandle, ofType: UIView.self)
}
}