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:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"platforms": ["ios"],
|
||||
"ios": {
|
||||
"modules": ["ExpoScrollEdgeInteractionModule"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type {ScrollEdgeInteractionEdge} from './src/ExpoScrollEdgeInteraction.types'
|
||||
export {ExpoScrollEdgeInteractionView} from './src/ExpoScrollEdgeInteractionView'
|
||||
@@ -0,0 +1,21 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ExpoScrollEdgeInteraction'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'UIScrollEdgeElementContainerInteraction for React Native views'
|
||||
s.description = 'UIScrollEdgeElementContainerInteraction for React Native views'
|
||||
s.author = 'bluesky-social'
|
||||
s.homepage = 'https://github.com/bluesky-social/social-app'
|
||||
s.platforms = { :ios => '13.4', :tvos => '13.4' }
|
||||
s.source = { git: '' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
}
|
||||
|
||||
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class ExpoScrollEdgeInteractionModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoScrollEdgeInteraction")
|
||||
|
||||
View(ExpoScrollEdgeInteractionView.self) {
|
||||
Prop("nodeHandle") { (view: ExpoScrollEdgeInteractionView, prop: Int?) in
|
||||
view.nodeHandle = prop
|
||||
}
|
||||
Prop("scrollViewTag") { (view: ExpoScrollEdgeInteractionView, prop: Int?) in
|
||||
view.scrollViewTag = prop
|
||||
}
|
||||
Prop("edge") { (view: ExpoScrollEdgeInteractionView, prop: String?) in
|
||||
view.edge = prop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {type ViewStyle} from 'react-native'
|
||||
|
||||
export type ScrollEdgeInteractionEdge = 'top' | 'bottom' | 'left' | 'right'
|
||||
|
||||
export interface ExpoScrollEdgeInteractionViewProps {
|
||||
nodeHandle: number | null
|
||||
scrollViewTag: number | null
|
||||
edge?: ScrollEdgeInteractionEdge
|
||||
children: React.ReactNode
|
||||
style?: ViewStyle
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import {requireNativeViewManager} from 'expo-modules-core'
|
||||
|
||||
import {type ExpoScrollEdgeInteractionViewProps} from './ExpoScrollEdgeInteraction.types'
|
||||
|
||||
const NativeView: React.ComponentType<ExpoScrollEdgeInteractionViewProps> =
|
||||
requireNativeViewManager('ExpoScrollEdgeInteraction')
|
||||
|
||||
export function ExpoScrollEdgeInteractionView({
|
||||
children,
|
||||
...rest
|
||||
}: ExpoScrollEdgeInteractionViewProps) {
|
||||
return <NativeView {...rest}>{children}</NativeView>
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import {type ExpoScrollEdgeInteractionViewProps} from './ExpoScrollEdgeInteraction.types'
|
||||
|
||||
export function ExpoScrollEdgeInteractionView({
|
||||
children,
|
||||
}: React.PropsWithChildren<ExpoScrollEdgeInteractionViewProps>) {
|
||||
return children
|
||||
}
|
||||
Reference in New Issue
Block a user