diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78ce9e9..56a228d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,13 +95,20 @@ jobs: with: xcode-version: ${{ env.XCODE_VERSION }} + - name: Cache DerivedData + if: env.turbo_cache_hit != 1 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/Library/Developer/Xcode/DerivedData + key: ${{ runner.os }}-deriveddata-${{ hashFiles('example/ios/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-deriveddata- + - name: Install cocoapods - if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' + if: env.turbo_cache_hit != 1 run: | cd example - bundle install - bundle exec pod repo update --verbose - bundle exec pod install --project-directory=ios + pod install --project-directory=ios - name: Build example for iOS run: | diff --git a/.gitignore b/.gitignore index 67f3212..9036817 100644 --- a/.gitignore +++ b/.gitignore @@ -41,9 +41,11 @@ project.xcworkspace local.properties android.iml -# Cocoapods -# -example/ios/Pods +# Example app Android (iOS-only library) +example/android/ + +# iOS example (generated by expo prebuild + pod install) +example/ios/ # Ruby example/vendor/ @@ -55,11 +57,6 @@ npm-debug.log yarn-debug.log yarn-error.log -# BUCK -buck-out/ -\.buckd/ -android/app/libs -android/keystores/debug.keystore # Yarn .yarn/* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a5de1ac --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## 0.1.0 + +### Added +- `ScrollEdgeBar` — a React Native container component that wraps any scroll view with sticky top and/or bottom bars +- `ScrollEdgeBar.TopBar` and `ScrollEdgeBar.BottomBar` slot components for placing bar content +- iOS 26+: bars use the system glass blur effect via `safeAreaBar`, seamlessly extending the navigation bar or tab bar +- iOS 16–25: bars are displayed using `safeAreaInset` (same layout, no blur effect) +- `estimatedTopBarHeight` / `estimatedBottomBarHeight` props to prevent flicker before layout +- TypeScript types and a compound React component API +- Full Fabric (New Architecture) support diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a23a386..2b88b7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,9 +27,7 @@ The [example app](/example/) demonstrates usage of the library. You need to run It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app. -If you want to use Android Studio or Xcode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/ScrollEdgeBarExample.xcworkspace` in Xcode and find the source files at `Pods > Development Pods > react-native-scroll-edge-bar`. - -To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `react-native-scroll-edge-bar` under `Android`. +If you want to use Xcode to edit the native code, open `example/ios/ScrollEdgeBarExample.xcworkspace` and find the source files at `Pods > Development Pods > react-native-scroll-edge-bar`. You can use various commands from the root directory to work with the project. @@ -39,12 +37,6 @@ To start the packager: yarn example start ``` -To run the example app on Android: - -```sh -yarn example android -``` - To run the example app on iOS: ```sh diff --git a/README.md b/README.md index 781c45e..2ae146f 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,82 @@ # react-native-scroll-edge-bar -`react-native-scroll-edge-bar` is a Fabric-based React Native view for attaching custom top and bottom bars to a scroll view on iOS. +React Native scroll edge bars for iOS — floating top and bottom bars that blend with the navigation bar or tab bar as the user scrolls. -It currently targets the iOS 26 `safeAreaBar` APIs and is implemented only on iOS. +--- -## Current Scope +## Why This Library Exists -- iOS implementation: present -- Android implementation: not implemented -- Fabric / New Architecture: required -- Native dependency: none beyond standard React Native Fabric/codegen on iOS +As of iOS 26, there is no direct way from React Native to attach a custom bar to a scroll view that blends with the system navigation or tab bar blur. The underlying mechanics live in SwiftUI's [`safeAreaBar`](https://developer.apple.com/documentation/swiftui/view/safeareabar(_:content:)), which coordinates with the system bars without requiring shared view hierarchy. -## Public API +This library bridges that gap by wrapping the native [`ScrollEdgeBar`](https://github.com/jensvansteen/ScrollEdgeBar) UIKit package, which itself drives the effect through SwiftUI. On iOS 16–25 it falls back to [`safeAreaInset`](https://developer.apple.com/documentation/swiftui/view/safeareainset(edge:alignment:spacing:content:))-style bars with the same layout, without the blur. -The library exports a compound component: +## Features -```tsx -import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; +- **Seamless glass blur** — top and bottom bars extend the navigation bar and tab bar Liquid Glass (iOS 26+) +- **Graceful fallback** — uses `safeAreaInset` on iOS 16–25, same layout without the blur +- **Top & bottom bars** — attach a bar to either scroll edge, or both +- **React Native Fabric** — full New Architecture support +- **Compound component API** — `ScrollEdgeBar`, `ScrollEdgeBar.TopBar`, `ScrollEdgeBar.BottomBar` + +## Requirements + +- iOS 16.0+ +- React Native New Architecture (Fabric) + +## Installation + +```sh +npm install react-native-scroll-edge-bar ``` -Available components: +Then install iOS pods in your app: -- `ScrollEdgeBar` -- `ScrollEdgeBar.TopBar` -- `ScrollEdgeBar.BottomBar` +```sh +cd ios && pod install +``` -### `ScrollEdgeBar` props +### Expo prebuild apps -- `estimatedTopBarHeight?: number` - - Fallback height used before the top bar has a measured layout. -- `estimatedBottomBarHeight?: number` - - Fallback height used before the bottom bar has a measured layout. -- `topBarOffset?: number` - - Extra offset to push the top bar below an external header. -- `bottomBarOffset?: number` - - Extra offset to lift the bottom bar above an external tab bar. -- Standard RN view props such as `style` +Set the iOS deployment target in your Expo config so regenerated native projects keep the required minimum: -### `ScrollEdgeBar.TopBar` props +```sh +npx expo install expo-build-properties +``` -- Standard RN view props such as `style` -- `children` +```json +{ + "expo": { + "plugins": [ + [ + "expo-build-properties", + { + "ios": { + "deploymentTarget": "16.0" + } + } + ] + ] + } +} +``` -### `ScrollEdgeBar.BottomBar` props +Then regenerate and run: -- Standard RN view props such as `style` -- `children` +```sh +npx expo prebuild --platform ios +npx expo run:ios +``` ## Usage -Basic usage: - ```tsx -import React from 'react'; import { ScrollView, Text, View } from 'react-native'; import SegmentedControl from '@react-native-segmented-control/segmented-control'; import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; export function Example() { return ( - + @@ -84,53 +97,164 @@ export function Example() { } ``` -If your navigation header or tab bar is external to the scroll-edge-bar container, you can provide explicit offsets: +## API Reference + +### `ScrollEdgeBar` + +| Prop | Type | Default | Description | +|---|---|---|---| +| `prefersGlassEffect` | `boolean` | `true` | When `false`, uses plain `safeAreaInset` bars on all OS versions. | +| `topEdgeEffectStyle` | `'automatic' \| 'soft' \| 'hard'` | `'automatic'` | iOS 26 scroll-edge effect intensity for the top edge. | +| `bottomEdgeEffectStyle` | `'automatic' \| 'soft' \| 'hard'` | `'automatic'` | iOS 26 scroll-edge effect intensity for the bottom edge. | +| `style` | `StyleProp` | — | Standard RN view style. | + +#### Advanced + +These props are not needed in most cases. The native package measures bar content automatically before the first frame. + +| Prop | Type | Default | Description | +|---|---|---|---| +| `estimatedTopBarHeight` | `number` | `0` | Layout hint before top bar is measured. Reduces first-frame flicker. | +| `estimatedBottomBarHeight` | `number` | `0` | Layout hint before bottom bar is measured. Reduces first-frame flicker. | + +### `ScrollEdgeBar.TopBar` / `ScrollEdgeBar.BottomBar` + +Accept standard RN view props (`style`, `children`). + +## Examples + +### App Store Listing + +Segmented control as a top bar above a ranked app list. The bar blends with the navigation bar as content scrolls beneath it. + + + +### App Store (No Glass) + +Same screen with `prefersGlassEffect={false}`, showing the plain `safeAreaInset` bar without the blur effect. + + + +### Pull Requests + +Horizontally scrolling filter chips as a top bar with a large title navigation bar. + + + +> **Note:** When a `safeAreaBar` is present alongside a large title navigation bar, SwiftUI applies the scroll edge blur effect to the navigation bar even when the content is at rest, causing it to appear blurry on first appearance. This is a known SwiftUI behavior ([FB21613303](https://developer.apple.com/forums/thread/812480)). + +### PR Detail + +Glass-effect review banner as a top bar and action buttons as a bottom bar. + + + +### Transition Showcase + +Large colored blocks demonstrating how the glass blur color transitions as you scroll past different background colors. + + + +### Toolbar + +Bottom edge bar positioned above the system toolbar. + + + +### Search Bar + +`UISearchController` in the navigation bar with a segmented control edge bar below it. + + + +### Calendar + +Week day selector as a top bar with a stronger scroll-edge effect. The effect intensity is controlled via `topEdgeEffectStyle`: ```tsx - + ... ``` -## Installation + -```sh -npm install react-native-scroll-edge-bar -``` +## Adaptive Bar Content -Then install iOS pods in your app: +The bar material/blur adapts to the content scrolling beneath it. However, React Native views rendered *inside* the bar do not automatically inherit the scroll-edge color transition — their colors stay as styled by React Native. -```sh -cd ios -pod install -``` +This matters most in a standalone bottom bar with no system `UITabBar` to merge into. When a bar merges with a `UINavigationBar` or `UITabBar`, RN content tends to integrate better due to the stronger system scroll-edge context. SwiftUI-backed content (e.g. via `@expo/ui`) adapts correctly in both cases because it is hosted through SwiftUI's own rendering path. + +Regular RN children are flexible and require no extra dependency: + +```tsx +import { PlatformColor, ScrollView, StyleSheet, Switch, Text, View } from 'react-native'; +import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; -## Example App +export function RNBottomBarExample() { + return ( + + {/* content */} + + + Test + + + + Reset + + + + ); +} -The example app in `example/` currently demonstrates: +const styles = StyleSheet.create({ + bottomBar: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 16, paddingVertical: 12, backgroundColor: 'transparent' }, + label: { fontSize: 15, fontWeight: '600', color: PlatformColor('label') }, + spacer: { flex: 1 }, + button: { paddingHorizontal: 14, paddingVertical: 8, borderRadius: 10, backgroundColor: PlatformColor('secondarySystemFill') }, + buttonText: { fontSize: 13, fontWeight: '600', color: PlatformColor('label') }, +}); +``` -- a native stack header via React Navigation native stack -- a native bottom tab bar via React Navigation native bottom tabs -- a `ScrollEdgeBar.TopBar` with a segmented control -- a `ScrollEdgeBar.BottomBar` with a label and switch +SwiftUI-backed controls participate in the scroll-edge transition more like native UIKit/SwiftUI controls. [`@expo/ui`](https://docs.expo.dev/versions/latest/sdk/ui/) is one option: -## Current Limitations +```tsx +import { ScrollView } from 'react-native'; +import { Button, HStack, Host, Spacer, Text, Toggle } from '@expo/ui/swift-ui'; +import { buttonStyle, controlSize, font, foregroundStyle, padding } from '@expo/ui/swift-ui/modifiers'; +import { ScrollEdgeBar } from 'react-native-scroll-edge-bar'; -- iOS-only in practice -- relies on view discovery and reparenting in Fabric, which is more fragile than a pure UIKit setup -- the implementation is currently tuned around iOS 26 APIs +export function SwiftUIBottomBarExample() { + return ( + + {/* content */} + + + + + + Test + + + +