fix expo-scroll-edge-effect not finding the scrollview

This commit is contained in:
Oleksii Bulenok
2026-06-26 17:26:57 +02:00
parent 870d899ab2
commit e9b81b303a
4 changed files with 134 additions and 0 deletions
@@ -0,0 +1,88 @@
diff --git a/ios/ExpoScrollEdgeEffectView.swift b/ios/ExpoScrollEdgeEffectView.swift
index 44b03cc7c33e770585e232635b7db2dfd21aee8b..6b24e55cf97c96142e53172dc9097c4e054d4d53 100644
--- a/ios/ExpoScrollEdgeEffectView.swift
+++ b/ios/ExpoScrollEdgeEffectView.swift
@@ -4,10 +4,14 @@ import UIKit
class ExpoScrollEdgeEffectView: ExpoView {
private var currentInteraction: NSObject?
+ private var isAttached = false
+ private var resolveAttempts = 0
+ private let maxResolveAttempts = 30
+
var scrollViewTag: Int? {
didSet {
if scrollViewTag != oldValue {
- updateInteraction()
+ scheduleResolve()
}
}
}
@@ -15,7 +19,7 @@ class ExpoScrollEdgeEffectView: ExpoView {
var edge: String = "top" {
didSet {
if edge != oldValue {
- updateInteraction()
+ scheduleResolve()
}
}
}
@@ -23,7 +27,7 @@ class ExpoScrollEdgeEffectView: ExpoView {
var effect: String = "automatic" {
didSet {
if effect != oldValue {
- updateInteraction()
+ scheduleResolve()
}
}
}
@@ -42,17 +46,39 @@ class ExpoScrollEdgeEffectView: ExpoView {
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil {
+ scheduleResolve()
+ }
+ }
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+
+ if !isAttached, window != nil, scrollViewTag != nil, resolveAttempts < maxResolveAttempts {
updateInteraction()
}
}
+ private func scheduleResolve() {
+ resolveAttempts = 0
+ updateInteraction()
+ }
+
private func updateInteraction() {
removeInteraction()
guard window != nil else { return }
guard let tag = scrollViewTag else { return }
guard #available(iOS 26, *) else { return }
- guard let scrollView = resolveScrollView(tag: tag) else { return }
+ guard let scrollView = resolveScrollView(tag: tag) else {
+ if resolveAttempts < maxResolveAttempts {
+ resolveAttempts += 1
+ setNeedsLayout()
+ }
+ return
+ }
+
+ resolveAttempts = 0
+ isAttached = true
let resolvedEdge = self.resolveEdge(edge)
let resolvedStyle = self.resolveEffectStyle(effect)
@@ -79,6 +105,8 @@ class ExpoScrollEdgeEffectView: ExpoView {
}
private func removeInteraction() {
+ isAttached = false
+
guard #available(iOS 26, *) else { return }
if let interaction = currentInteraction as? UIScrollEdgeElementContainerInteraction {
@@ -0,0 +1,44 @@
# @bsky.app\_\_expo-scroll-edge-effect.patch
Fixes the iOS 26 scroll edge effect (the blur under the status bar) failing to
attach on the New Architecture (Fabric).
## Why
The native view resolves the target scroll view by React tag:
`scrollViewTag` (published from JS when the scroll view mounts) ->
`appContext.findView(withTag:)` -> attach a `UIScrollEdgeElementContainerInteraction`.
On Paper this resolved on the first try, because view-by-tag registration was
serialized through the single UIManager batch pipeline - by the time the tag
prop reached native, the scroll view was already in the registry.
On Fabric there is no synchronous tag->view registry. The scroll view's
`ComponentView` is registered when its mount transaction is applied, which is
deferred relative to the JS commit that set the tag. So the one-shot
`resolveScrollView` runs before the view is mounted and returns nil, the
interaction never attaches, and the blur is lost. (The un-gated case happened to
work only because the header's `didMoveToWindow` provided a late second attempt;
a list mounted later - e.g. behind an `isActive` gate - had no such retry.)
## What it does
Makes resolution event-driven and self-healing instead of one-shot:
- When `resolveScrollView` returns nil, it bumps a bounded counter and calls
`setNeedsLayout()`; `layoutSubviews()` re-attempts as sibling views mount, so
it retries exactly when the scroll view actually appears - no fixed timer.
- `isAttached` stops re-running once resolved (no teardown/re-attach churn).
- `resolveAttempts` / `maxResolveAttempts` bound the retries so a
permanently-absent scroll view (list not rendered) cannot spin layout forever.
- The budget is reset on every "fresh" trigger (prop change, entering the
window) via `scheduleResolve()`, so each new tag gets a full retry budget.
## Notes
- iOS only; gated behind `#available(iOS 26, *)`.
- This mirrors how `react-native-screens` handles the same effect on Fabric
(resolve from the live view hierarchy and re-apply on mount/layout lifecycle
rather than a single tag lookup). A cleaner long-term fix would move
resolution to the scroll-view side, but this is the minimal change to the
existing tag-based design.
+1
View File
@@ -215,6 +215,7 @@ overrides:
react-native-screens: 4.24.0
patchedDependencies:
'@bsky.app/expo-scroll-edge-effect': b745392cadb2a707a0b6fcd12de9dc3313765aaf6de089a4a69e6b910a19d724
'@sentry/react-native@6.20.0': 1d48e4d5178f5684eb33262299e7eed283bf9601f79b9ae1af63a7f607d3483a
expo-age-range@0.2.18: c325225749993424461eeece1d97a99b19c00da2ebc958a73c12e4eca0c39306
expo-glass-effect@55.0.8: 6c9fa5b104e53b87df4e17b320acb3b94d12ac90c0101190045dd620681efff0
+1
View File
@@ -19,6 +19,7 @@ allowBuilds:
'esbuild': true
'unrs-resolver': true
patchedDependencies:
'@bsky.app/expo-scroll-edge-effect': patches/@bsky.app__expo-scroll-edge-effect.patch
'@sentry/react-native@6.20.0': patches/@sentry__react-native@6.20.0.patch
expo-age-range@0.2.18: patches/expo-age-range@0.2.18.patch
'expo-haptics@15.0.8': patches/expo-haptics@15.0.8.patch