Skip to content

Commit 4e1d7c1

Browse files
authored
fix(iOS, FormSheet): Add fallback for searching ScrollView which is inside SAV (#3479)
## Description Fixes a regression in ScrollView with non-scrolling content when using absolute styles. Adding a SAV into the hierarchy - as a workaround to content's padding issues under the header on iOS 26 - broke the logic for finding the ScrollView inside the `ScreenContentWrapper`, preventing the frame correction from being applied properly. This PR adds fallbacks to search for the `ScrollView` within the subviews of the `SafeAreaView`. > [!NOTE] > Allowing the `flex:1` style for the `ContentWrapper` also resolves an issue with ScrollView's `contentInsetsAdjustmentBehavior`, which now works correctly with synchronous state updates enabled. It would be good to align this behavior for the `absoluteWithNoBottom` style as well - software-mansion/react-native-screens-labs#718 ## Changes - Added fallback logic to search within `SafeAreaView` subviews for `ScrollView` in `ContentWrapper`. ## Screenshots / GIFs Here you can add screenshots / GIFs documenting your change. You can add before / after section if you're changing some behavior. ### Before <table> <tr> <td width="50%"> <video src="https://github.com/user-attachments/assets/013ea98e-c4ec-4c96-8963-4f2c6829da61"></video> </td> <td width="50%"> <video src="https://github.com/user-attachments/assets/acef493d-a37e-482f-be67-a5c2a8ea746c"></video> </td> </tr> </table> ### After <table> <tr> <td width="50%"> <video src="https://github.com/user-attachments/assets/7d96a236-4ba6-4b6a-8c1f-8f64871b1faa"></video> </td> <td width="50%"> <video src="https://github.com/user-attachments/assets/3bbd69e1-8760-4246-9a21-50eb48e3e54a"></video> </td> </tr> </table> ## Test code and steps to reproduce Tested on TestFormSheet ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes
1 parent c9b84ff commit 4e1d7c1

File tree

3 files changed

+56
-11
lines changed

3 files changed

+56
-11
lines changed

ios/RNSScreen.mm

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#import <React/RCTUIManagerUtils.h>
3131

3232
#import "RNSConversions.h"
33+
#import "RNSSafeAreaViewComponentView.h"
3334
#import "RNSSafeAreaViewNotifications.h"
3435
#import "RNSScreenFooter.h"
3536
#import "RNSScreenStack.h"
@@ -902,7 +903,8 @@ - (nullable RNS_REACT_SCROLL_VIEW_COMPONENT *)tryFindDescendantScrollView
902903
// Step 1: Query registered content wrapper for the scrollview.
903904
RNSScreenContentWrapper *contentWrapper = _contentWrapperBox.contentWrapper;
904905

905-
if (RNS_REACT_SCROLL_VIEW_COMPONENT *_Nullable scrollViewComponent = [contentWrapper childRCTScrollViewComponent];
906+
if (RNS_REACT_SCROLL_VIEW_COMPONENT *_Nullable scrollViewComponent =
907+
[contentWrapper childRCTScrollViewComponentAndContentContainer].scrollViewComponent;
906908
scrollViewComponent != nil) {
907909
return scrollViewComponent;
908910
}
@@ -916,6 +918,20 @@ - (nullable RNS_REACT_SCROLL_VIEW_COMPONENT *)tryFindDescendantScrollView
916918
}
917919
}
918920

921+
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
922+
// Fallback 2: Search through RNSSafeAreaViewComponentView subviews (iOS 26+ workaround with modified hierarchy)
923+
if (@available(iOS 26.0, *)) {
924+
UIView *maybeSafeAreaView = contentWrapper.subviews.firstObject;
925+
if ([maybeSafeAreaView isKindOfClass:RNSSafeAreaViewComponentView.class]) {
926+
for (UIView *subview in maybeSafeAreaView.subviews) {
927+
if ([subview isKindOfClass:RNS_REACT_SCROLL_VIEW_COMPONENT.class]) {
928+
return static_cast<RNS_REACT_SCROLL_VIEW_COMPONENT *>(subview);
929+
}
930+
}
931+
}
932+
}
933+
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
934+
919935
return nil;
920936
}
921937

ios/RNSScreenContentWrapper.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ NS_ASSUME_NONNULL_BEGIN
2525

2626
@end
2727

28+
typedef struct {
29+
RNS_REACT_SCROLL_VIEW_COMPONENT *scrollViewComponent;
30+
UIView *contentContainerView;
31+
} RNSScrollViewSearchResult;
32+
2833
@interface RNSScreenContentWrapper :
2934
#ifdef RCT_NEW_ARCH_ENABLED
3035
RCTViewComponentView
@@ -39,7 +44,7 @@ NS_ASSUME_NONNULL_BEGIN
3944
*/
4045
- (void)triggerDelegateUpdate;
4146

42-
- (nullable RNS_REACT_SCROLL_VIEW_COMPONENT *)childRCTScrollViewComponent;
47+
- (RNSScrollViewSearchResult)childRCTScrollViewComponentAndContentContainer;
4348

4449
- (BOOL)coerceChildScrollViewComponentSizeToSize:(CGSize)size;
4550

ios/RNSScreenContentWrapper.mm

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#import "RNSScreenContentWrapper.h"
22
#import "RNSDefines.h"
3+
#import "RNSSafeAreaViewComponentView.h"
34
#import "RNSScreen.h"
45
#import "RNSScreenStack.h"
56

@@ -106,32 +107,55 @@ - (nullable RNSScreenView *)findFirstScreenViewAncestor
106107
return static_cast<RNSScreenView *_Nullable>(currentView);
107108
}
108109

109-
- (nullable RNS_REACT_SCROLL_VIEW_COMPONENT *)childRCTScrollViewComponent
110+
- (RNSScrollViewSearchResult)childRCTScrollViewComponentAndContentContainer
110111
{
112+
// Directly search subviews
111113
for (UIView *subview in self.subviews) {
112114
if ([subview isKindOfClass:RNS_REACT_SCROLL_VIEW_COMPONENT.class]) {
113-
return static_cast<RNS_REACT_SCROLL_VIEW_COMPONENT *>(subview);
115+
return (RNSScrollViewSearchResult){.scrollViewComponent = static_cast<RNS_REACT_SCROLL_VIEW_COMPONENT *>(subview),
116+
.contentContainerView = self};
114117
}
115118
}
116-
return nil;
119+
120+
// Fallback 1: Search through RNSSafeAreaViewComponentView subviews (iOS 26+ workaround with modified hierarchy)
121+
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
122+
if (@available(iOS 26.0, *)) {
123+
UIView *maybeSafeAreaView = self.subviews.firstObject;
124+
if ([maybeSafeAreaView isKindOfClass:RNSSafeAreaViewComponentView.class]) {
125+
for (UIView *subview in maybeSafeAreaView.subviews) {
126+
if ([subview isKindOfClass:RNS_REACT_SCROLL_VIEW_COMPONENT.class]) {
127+
return (RNSScrollViewSearchResult){
128+
.scrollViewComponent = static_cast<RNS_REACT_SCROLL_VIEW_COMPONENT *>(subview),
129+
.contentContainerView = maybeSafeAreaView};
130+
}
131+
}
132+
}
133+
}
134+
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
135+
136+
return (RNSScrollViewSearchResult){.scrollViewComponent = nullptr, .contentContainerView = nullptr};
117137
}
118138

119139
- (BOOL)coerceChildScrollViewComponentSizeToSize:(CGSize)size
120140
{
121-
RNS_REACT_SCROLL_VIEW_COMPONENT *_Nullable scrollViewComponent = [self childRCTScrollViewComponent];
141+
auto scrollViewComponentAndContentContainerPair = [self childRCTScrollViewComponentAndContentContainer];
142+
RNS_REACT_SCROLL_VIEW_COMPONENT *_Nullable scrollViewComponent =
143+
scrollViewComponentAndContentContainerPair.scrollViewComponent;
144+
UIView *_Nullable containerView = scrollViewComponentAndContentContainerPair.contentContainerView;
122145

123146
if (scrollViewComponent == nil) {
124147
return NO;
125148
}
126149

127-
if (self.subviews.count > 2) {
150+
if (containerView.subviews.count > 2) {
128151
RCTLogWarn(
129-
@"[RNScreens] FormSheet with ScrollView expects at most 2 subviews. Got %ld. This might result in incorrect layout. \
152+
@"[RNScreens] FormSheet with ScrollView expects at most 2 subviews. Got %ld for container: %@. This might result in incorrect layout. \
130153
If you want to display header alongside the scrollView, make sure to apply `collapsable: false` on your header component view.",
131-
self.subviews.count);
154+
containerView.subviews.count,
155+
NSStringFromClass(containerView.class));
132156
}
133157

134-
NSUInteger scrollViewComponentIndex = [[self subviews] indexOfObject:scrollViewComponent];
158+
NSUInteger scrollViewComponentIndex = [containerView.subviews indexOfObject:scrollViewComponent];
135159

136160
// Case 1: ScrollView first child - takes whole size.
137161
if (scrollViewComponentIndex == 0) {
@@ -143,7 +167,7 @@ - (BOOL)coerceChildScrollViewComponentSizeToSize:(CGSize)size
143167

144168
// Case 2: There is a header - we adjust scrollview size by the header height.
145169
if (scrollViewComponentIndex == 1) {
146-
UIView *headerView = self.subviews[0];
170+
UIView *headerView = containerView.subviews[0];
147171
CGRect newFrame = scrollViewComponent.frame;
148172
newFrame.size = size;
149173
newFrame.size.height -= headerView.frame.size.height;

0 commit comments

Comments
 (0)