Pull to refresh
A platform-split recipe — iOS and Android overscroll differently.
Because content is padded below the header, the native RefreshControl spinner renders at the content origin — tucked behind the header. The robust recipe differs by platform, because the two platforms overscroll differently.
⚠ Android — lists don’t bounce
The offset stays clamped at 0 (you get a stretch/glow), so an offset-driven custom pull can’t work. Use the native RefreshControl and let the adapter’s default progressViewOffset (= header height) push its spinner below the header, or set it yourself.
⚠ iOS — lists bounce
contentOffset.y goes negative past the top. Read that pull distance from useCurrentTabScrollY() and drive your own spinner pinned in the visible area (below the header) — the native iOS spinner would be hidden behind the header.
const scrollY = useCurrentTabScrollY(); // negative on iOS = pull distance
const { height } = useHeaderMeasurements();
<Tabs.FlatList
refreshControl={
Platform.OS === 'android'
? <RefreshControl refreshing={busy} onRefresh={refresh}
progressViewOffset={height} />
: undefined // iOS: render a custom spinner driven by scrollY instead
}
/>