forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1# expo-scroll-forwarder
2
3An Expo native module that forwards scroll gestures from a UIView to a UIScrollView on iOS. This enables custom scroll behaviors by allowing a non-scrollable view to control a scrollable view's scroll position.
4
5## What It Does
6
7This module solves a specific interaction problem: allowing a fixed header or overlay view to respond to scroll gestures and forward them to an underlying scroll view. The primary use case in the Bluesky app is the profile screen, where the profile header sits above a scrollable content area and can be dragged to scroll the content below it.
8
9Key behaviors:
10- Captures pan gestures on a wrapper view and translates them to scroll offsets on a target scroll view
11- Implements physics-based deceleration animations that match native scroll behavior
12- Supports pull-to-refresh interactions with haptic feedback
13- Prevents gesture conflicts with iOS swipe-back navigation by only activating on vertical pans
14- Provides rubber-band damping when scrolling past content bounds
15
16## Architecture
17
18The module consists of three main parts:
19
20### 1. Native iOS Implementation (Swift)
21
22**ExpoScrollForwarderView.swift** - The core native view component that:
23- Attaches a UIPanGestureRecognizer to intercept scroll gestures
24- Finds and references the target RCTScrollView using its React Native tag
25- Implements custom scroll physics including velocity-based decay animation
26- Manages gesture recognizer delegation to prevent conflicts with system gestures
27- Handles pull-to-refresh activation at -130pt scroll offset with haptic feedback
28
29**ExpoScrollForwarderModule.swift** - The Expo module definition that:
30- Registers the view component with Expo
31- Exposes the `scrollViewTag` prop to specify which scroll view to control
32
33### 2. TypeScript Interface
34
35**ExpoScrollForwarderView.tsx** - Platform-specific implementations:
36- **iOS (.ios.tsx)**: Wraps the native view manager from expo-modules-core
37- **Default (.tsx)**: No-op wrapper that just renders children (for Android/Web compatibility)
38
39**ExpoScrollForwarder.types.ts** - TypeScript type definitions:
40- `scrollViewTag`: The React Native tag of the scroll view to control
41- `children`: The content to render (typically a header component)
42
43### 3. Module Configuration
44
45**expo-module.config.json** - Declares iOS-only platform support
46
47**ExpoScrollForwarder.podspec** - CocoaPods specification for iOS dependency management
48
49## Usage
50
51```tsx
52import {ExpoScrollForwarderView} from 'expo-scroll-forwarder'
53
54function ProfileScreen() {
55 const scrollViewTag = useRef(null)
56
57 return (
58 <View>
59 <ExpoScrollForwarderView scrollViewTag={scrollViewTag.current}>
60 <ProfileHeader />
61 </ExpoScrollForwarderView>
62
63 <ScrollView ref={scrollViewTag}>
64 {/* Scrollable content */}
65 </ScrollView>
66 </View>
67 )
68}
69```
70
71The `scrollViewTag` prop must be the React Native tag (numeric identifier) of the target scroll view. The module uses this to locate the native UIScrollView instance.
72
73## Platform Support
74
75- **iOS**: Full native implementation with custom scroll physics
76- **Android**: No-op wrapper (renders children without scroll forwarding)
77- **Web**: No-op wrapper (renders children without scroll forwarding)
78
79The module is designed to enhance iOS UX while gracefully degrading on other platforms.
80
81## Key Implementation Details
82
83### Gesture Recognition
84- Only activates when pan velocity is more vertical than horizontal (`abs(velocity.y) > abs(velocity.x)`)
85- Delegates to UIGestureRecognizerDelegate to prevent simultaneous recognition with navigation swipe-back
86- Adds tap/long-press recognizers to the scroll view to cancel ongoing animations
87
88### Scroll Physics
89- Implements custom decay animation at 120fps using a Timer
90- Velocity decay factor: 0.9875 per frame
91- Velocity clamped to +/- 5000 points/second
92- Rubber-band damping: offsets below 0 are reduced by 55%
93- Animation stops when velocity drops below 5 points/second
94
95### Pull-to-Refresh
96- Triggers at -130pt scroll offset
97- Provides haptic feedback (UIImpactFeedbackGenerator, light style)
98- Calls refresh control via `RCTRefreshControl.forwarderBeginRefreshing()`
99
100### Scroll View Management
101- Dynamically finds scroll view using `AppContext.findView(withTag:ofType:)`
102- Properly cleans up gesture recognizers when switching between scroll views
103- Maintains references to both the scroll view and its refresh control
104
105## Files Overview
106
107| File | Purpose |
108|------|---------|
109| `ios/ExpoScrollForwarderView.swift` | Native iOS view implementation with gesture handling and scroll physics |
110| `ios/ExpoScrollForwarderModule.swift` | Expo module registration and prop definitions |
111| `ios/ExpoScrollForwarder.podspec` | CocoaPods dependency specification |
112| `src/ExpoScrollForwarderView.ios.tsx` | TypeScript wrapper for iOS native view |
113| `src/ExpoScrollForwarderView.tsx` | Default no-op implementation for other platforms |
114| `src/ExpoScrollForwarder.types.ts` | TypeScript type definitions |
115| `index.ts` | Module entry point |
116| `expo-module.config.json` | Expo module configuration |