fix(iOS): add fall back to window.safeAreaInsets when no ancestor provider is found - #4440
fix(iOS): add fall back to window.safeAreaInsets when no ancestor provider is found #4440Loloekk wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes iOS SafeAreaView (experimental) so it still applies safe-area insets when no ancestor safe-area provider is present by falling back to window.safeAreaInsets, avoiding the infinite layout loop that would occur when using the view itself as a provider.
Changes:
- iOS: Trigger inset state updates via
safeAreaInsetsDidChangewhen no provider exists, and compute insets fromwindow.safeAreaInsetsas a stable fallback. - Apps: Add a minimal repro scenario (
Test4440) demonstratingSafeAreaViewbehavior without a provider. - Apps: Register the new issue-test export in the issue-tests index.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| ios/safe-area/RNSSafeAreaViewComponentView.mm | Adds window-based fallback insets and updates state when safe-area changes with no provider. |
| apps/src/tests/issue-tests/Test4440.tsx | Adds a minimal test case for SafeAreaView without an ancestor provider. |
| apps/src/tests/issue-tests/index.ts | Exposes Test4440 via the issue-tests entrypoint. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // SafeAreaView applies insets as margin (not padding), so backgroundColor | ||
| // does not paint into the system safe-area — the parent shows through there. | ||
| <SafeAreaView | ||
| style={{ flex: 1, backgroundColor: 'blue' }} |
There was a problem hiding this comment.
nit: we have our own color palette, which you can use import { Colors } from '@apps/shared/styling';
| // does not paint into the system safe-area — the parent shows through there. | ||
| <SafeAreaView | ||
| style={{ flex: 1, backgroundColor: 'blue' }} | ||
| edges={{ top: true, bottom: true }}> |
There was a problem hiding this comment.
I'd extend this test a bit, allowing all 4 edges to be programmatically configurable using some Buttons and state, having an option to test more valid configurations in a single example.
|
|
||
| - (UIEdgeInsets)fallbackSafeAreaInsets | ||
| { | ||
| return self.window.safeAreaInsets; |
There was a problem hiding this comment.
I have a concern about this approach that window insets aren't respecting the view's position, while on native iOS own safeAreaInsets are calculated with respect to the view hierarchy. Reading the value from the window may incorrectly apply the margin in e.g. modals, which are positioned below the top inset.
I see the problem with using self as an insets fallback combined with applying margins instead of padding.
Right now I'm considering a different approach, which would be exposing an explicit SafeAreaProvider component to the public API that would implement RNSSafeAreaProviding with its own safeAreaInsets + change notifications, and log a warning when there's no provider at all. Anyway, I'd wait with the final decision for @kkafar, as he did much more research regarding SAV.
There was a problem hiding this comment.
I want to cc @kligarski here.
I'm not certain about right direction here. Would it make sense to simply make a root view controller of each hierarchy a default provider in such cases? wdyt?
There was a problem hiding this comment.
Would it make sense to simply make a root view controller of each hierarchy a default provider in such cases? wdyt?
I'm not sure what you mean here. We would need to make React's root view controller implement RNSSafeAreaProviding?
Regarding the issue highlighted by @t0maboro, I think I agree with the concern regarding the solution in this PR. I think we should consider either separate provider or changing how SAV work from margin to padding - I don't think we had any particular reason why we went with the margin approach but I might be forgetting something here.
Description
On iOS, SafeAreaView did not apply insets unless it had an ancestor provider such as Screen, tabs, or a stack container. SafeAreaView from react-native-safe-area-context similarly requires a SafeAreaProvider.
Using the SafeAreaView itself as a fallback provider (as RNSAC does with return self) does not work with our implementation. We apply insets as margin, which changes the view’s frame. UIKit then recomputes safeAreaInsets for that smaller frame, which updates the margin again and causes an infinite layout loop.
When no ancestor provider is found, this change falls back to window.safeAreaInsets. That gives stable system chrome insets (status bar, home indicator, etc.) without the feedback loop. This matches the intended no-provider case: there is no navigation chrome to account for, so window-level system insets are the correct source.
Closes https://github.com/software-mansion/react-native-screens-labs/issues/1661
Changes
ios/safe-area/RNSSafeAreaViewComponentView.mmto fall back to window insets when no ancestor provider is found, instead of skipping the updateBefore & after - visual documentation
Test plan
Test4440—SafeAreaViewfromreact-native-screens/experimentalwith no provider screen; verify insets on iOS.TabsContainerinsideSafeAreaView— works as expected (no overlaps or other layout issues).Checklist