Repository navigation
fix(ios): keep SafeAreaView insets current in native sheets after rotation - #803
Conversation
|
@hirbod is attempting to deploy a commit to the appandflow Team on Vercel. A member of the Team first needs to authorize it. |
floco-af
left a comment
There was a problem hiding this comment.
Thanks for the detailed repro and the slowed-down comparison, the two-part diagnosis makes sense. Review of the diff (read through; I have not run it on a device, so none of this is native-validated by me):
1. safeAreaInsetsDidChange override (fixes #781) – looks correct. It is gated on _providerView == self, so views under a real SafeAreaProvider still go through the notification path and don't get a duplicate update. updateStateIfNecessaryMeasuringCorners: already bails on a nil window and on unchanged insets, so repeated callbacks are cheap.
2. UpdateMode::unstable_Immediate (fixes #802) – a few concerns:
unstable_Immediatewas added toEventQueue::UpdateModein RN 0.82 (it isn't in 0.81 or earlier). The package'speerDependenciesisreact-native: "*", so this would break the iOS build for anyone on <0.82. It needs a version guard (e.g. a__has_include/RN version macro check, or a bump of the minimum supported RN) – please tell us which you'd prefer; we'd lean towards a guard.updateStateis also called fromfinalizeUpdates:anddidMoveToWindow, i.e. during a mounting transaction on the main thread.unstable_Immediateflushes the state update synchronously (flushStateUpdates()), which can commit a new shadow tree re-entrantly from inside mounting. react-native-screens only uses it behind an opt-in flag (_synchronousShadowStateUpdatesEnabled), not unconditionally. It would be safer to use Immediate only for thesafeAreaInsetsDidChange/ provider-notification path (the rotation case) and keep the default async mode for mount-time updates.- The file is shared with the macOS target, so the same RN-version question applies to react-native-macos.
3. Tests / validation – the before/after GIF is a good demonstration of the sheet case. It would be good to also confirm the non-sheet case (normal SafeAreaView under a provider, plain rotation, and a screen with many mounts) doesn't regress, and to note which RN version the fix was tested against (0.88.1 per the report).
Verdict: the approach looks right, but it needs the RN version guard and ideally narrowing the Immediate mode before merge. cc @janicduplessis
EventQueue::UpdateMode came in React Native 0.82, so passing unstable_Immediate broke the build on 0.81 and older. Detect it at compile time and fall back to the default update.
A SafeAreaView in a native formSheet, with and without a SafeAreaProvider, plus switches for a busy JS thread and for mounting 200 SafeAreaViews, to check rotation in sheets.
|
@floco-af thanks, pushed an update. RN version. Good catch. Immediate during mounting. Kept it. A commit that lands mid-transaction isn't nested, Tests. Added a Form Sheet example (dev menu) with a busy JS switch and a switch that mounts 200 SafeAreaViews. iPhone 18 Pro simulator, iOS 27, RN 0.85. The border clears the notch from the first frame after every rotation. To try it in the repro, use the
Also checked on a real device. Not tested on react-native-macos. |
floco-af
left a comment
There was a problem hiding this comment.
Thanks for the update, and for the form sheet example and the extra recordings.
RN compat guard – this resolves my concern. I compiled the SupportsImmediateStateUpdate detection (C++17, std::void_t) against a stub queue with and without UpdateMode::unstable_Immediate: it resolves to true_type and false_type respectively, so older RN (and anything without the enum value) falls back to the plain updateState call. Detecting the symbol rather than checking a version is also the right call for react-native-macos. I haven't built it against RN itself (no Xcode here), so I'm relying on your 0.81.6 / 0.85 builds for that.
Immediate during mounting – your argument that a commit landing mid-transaction is queued by RCTMountingManager and mounted right after is plausible, and matching what react-native-screens does is reasonable. I can't confirm the re-entrancy behaviour on a device, so I'd leave the final call on keeping it for all paths vs. narrowing it to the rotation path to the maintainer. The 200-SafeAreaView mounting case in your example is the right stress test for it.
Validation – the non-sheet, provider, busy-JS and mass-mount cases now cover what I asked for. Still not validated by me on a native build.
From my side the remaining items are addressed; the rest is a maintainer decision. cc @janicduplessis
janicduplessis
left a comment
There was a problem hiding this comment.
Looks good, thanks for the fix!






Summary
A
SafeAreaViewinside a native sheet (like a react-native-screensformSheet) shows stale insets after a rotation. There are two separate problems behind it.The sheet doesn't live under the provider's view tree, so the view falls back to being its own provider. Nothing tells it when its own insets change, so it keeps the old ones until React commits again (#781). Overriding
safeAreaInsetsDidChangefor that case fixes it.With that in place, the update still goes through the default async state mode and waits for the next event beat. In a busy app you see the old insets for a few frames (#802). react-native-screens has the same problem with its screen state and uses
unstable_Immediatefor it, so this does the same.You need both. With only one of them the insets are still wrong.
Closes #781
Closes #802
Test Plan
Repro is at https://github.com/hirbod/rnsac-rotation-repro (Expo SDK 58, RN 0.88.1, react-native-screens 4.30.1, iOS 27 simulator). Open the sheet, rotate to landscape left, then landscape right, then back to portrait.
Left is 5.11.0, middle only has the
safeAreaInsetsDidChangechange, right has both. Slowed down 4x.I also checked it in the sheets of my own app.