Skip to content

Commit 6978a6d

Browse files
authored
fix(iOS, Stack): center subview not visible/misaligned in header (#3489)
## Description Fixes missing/misaligned center subview in header on iOS. | before | after | | --- | --- | | <img width="1206" height="2622" alt="before" src="https://github.com/user-attachments/assets/b38a076e-3d80-42f5-8bd3-12a649e856f1" /> | <img width="1206" height="2622" alt="after" src="https://github.com/user-attachments/assets/81ddc53d-f6a2-46fc-9a5b-566c42828408" /> | Fixes #3482. ## Changes - limit wrapping subviews and related logic to left and right subviews only on iOS 26+ (previously, there was only an SDK macro, without runtime check) ## Test code and steps to reproduce Run `Test3446` to verify that pressables are still working. To test center subview, use: <details> <summary>Test example</summary> ```tsx import React from 'react'; import { View, Text } from 'react-native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import Colors from '../shared/styling/Colors'; function MainScreen() { return <View style={{ backgroundColor: 'moccasin' }} />; } const Stack = createNativeStackNavigator(); export default function App() { return ( <Stack.Navigator> <Stack.Screen name="Main" component={MainScreen} options={{ headerLeft: () => <Text>a</Text>, headerTitle: () => <Text>b</Text>, headerRight: () => <Text>c</Text>, headerBackground: () => ( <View style={{ backgroundColor: Colors.PurpleLight80, width: '100%', height: 120, }} /> ), }} /> </Stack.Navigator> ); } ``` </details> ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes
1 parent 952b7cd commit 6978a6d

File tree

1 file changed

+73
-44
lines changed

1 file changed

+73
-44
lines changed

ios/RNSScreenStackHeaderSubview.mm

Lines changed: 73 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,17 @@ - (void)updateLayoutMetrics:(const react::LayoutMetrics &)layoutMetrics
174174
self);
175175
} else {
176176
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
177-
BOOL sizeHasChanged = _layoutMetrics.frame.size != layoutMetrics.frame.size;
178-
_layoutMetrics = layoutMetrics;
179-
if (sizeHasChanged) {
180-
[self invalidateIntrinsicContentSize];
181-
}
182-
#else // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
183-
self.bounds = CGRect{CGPointZero, frame.size};
177+
if (self.needsAutoLayout) {
178+
BOOL sizeHasChanged = _layoutMetrics.frame.size != layoutMetrics.frame.size;
179+
_layoutMetrics = layoutMetrics;
180+
if (sizeHasChanged) {
181+
[self invalidateIntrinsicContentSize];
182+
}
183+
} else
184184
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
185+
{
186+
self.bounds = CGRect{CGPointZero, frame.size};
187+
}
185188
[self layoutNavigationBar];
186189
}
187190
}
@@ -206,56 +209,82 @@ - (void)reactSetFrame:(CGRect)frame
206209
// makes UINavigationBar the only one to control the position of header content.
207210
if (!CGSizeEqualToSize(frame.size, self.frame.size)) {
208211
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
209-
_lastReactFrameSize = frame.size;
210-
[self invalidateIntrinsicContentSize];
211-
#else // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
212-
[super reactSetFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
212+
if (self.needsAutoLayout) {
213+
_lastReactFrameSize = frame.size;
214+
[self invalidateIntrinsicContentSize];
215+
} else
213216
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
217+
{
218+
[super reactSetFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
219+
}
214220
[self layoutNavigationBar];
215221
}
216222
}
217223

218224
#endif // RCT_NEW_ARCH_ENABLED
219225

226+
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
227+
228+
// Starting from iOS 26, to center left and right subviews inside liquid glass backdrop,
229+
// we need to use auto layout. To make Yoga's layout work with auto layout, we pass information
230+
// from Yoga via `intrinsicContentSize`.
231+
- (BOOL)needsAutoLayout
232+
{
233+
BOOL needsAutoLayout = NO;
234+
if (@available(iOS 26.0, *)) {
235+
needsAutoLayout = _type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight;
236+
}
237+
return needsAutoLayout;
238+
}
239+
240+
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
241+
220242
#pragma mark - UIBarButtonItem specific
221243

222244
- (UIBarButtonItem *)getUIBarButtonItem
223245
{
246+
RCTAssert(
247+
_type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight,
248+
@"[RNScreens] Unexpected subview type.");
249+
224250
if (_barButtonItem == nil) {
225251
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
226-
// Starting from iOS 26, UIBarButtonItem's customView is streched to have at least 36 width.
227-
// Stretching RNSScreenStackHeaderSubview means that its subviews are aligned to left instead
228-
// of the center. To mitigate this, we add a wrapper view that will center
229-
// RNSScreenStackHeaderSubview inside of itself.
230-
UIView *wrapperView = [UIView new];
231-
wrapperView.translatesAutoresizingMaskIntoConstraints = NO;
232-
233-
self.translatesAutoresizingMaskIntoConstraints = NO;
234-
[wrapperView addSubview:self];
235-
236-
[self.centerXAnchor constraintEqualToAnchor:wrapperView.centerXAnchor].active = YES;
237-
[self.centerYAnchor constraintEqualToAnchor:wrapperView.centerYAnchor].active = YES;
238-
239-
// To prevent UIKit from stretching subviews to all available width, we need to:
240-
// 1. Set width of wrapperView to match RNSScreenStackHeaderSubview BUT when
241-
// RNSScreenStackHeaderSubview's width is smaller that minimal required 36 width, it breaks
242-
// UIKit's constraint. That's why we need to lower the priority of the constraint.
243-
NSLayoutConstraint *widthEqual = [wrapperView.widthAnchor constraintEqualToAnchor:self.widthAnchor];
244-
widthEqual.priority = UILayoutPriorityDefaultHigh;
245-
widthEqual.active = YES;
246-
247-
NSLayoutConstraint *heightEqual = [wrapperView.heightAnchor constraintEqualToAnchor:self.heightAnchor];
248-
heightEqual.priority = UILayoutPriorityDefaultHigh;
249-
heightEqual.active = YES;
250-
251-
// 2. Set content hugging priority for RNSScreenStackHeaderSubview.
252-
[self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
253-
[self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
254-
255-
_barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:wrapperView];
256-
#else // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
257-
_barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self];
252+
if (@available(iOS 26.0, *)) {
253+
// Starting from iOS 26, UIBarButtonItem's customView is streched to have at least 36 width.
254+
// Stretching RNSScreenStackHeaderSubview means that its subviews are aligned to left instead
255+
// of the center. To mitigate this, we add a wrapper view that will center
256+
// RNSScreenStackHeaderSubview inside of itself.
257+
UIView *wrapperView = [UIView new];
258+
wrapperView.translatesAutoresizingMaskIntoConstraints = NO;
259+
260+
self.translatesAutoresizingMaskIntoConstraints = NO;
261+
[wrapperView addSubview:self];
262+
263+
[self.centerXAnchor constraintEqualToAnchor:wrapperView.centerXAnchor].active = YES;
264+
[self.centerYAnchor constraintEqualToAnchor:wrapperView.centerYAnchor].active = YES;
265+
266+
// To prevent UIKit from stretching subviews to all available width, we need to:
267+
// 1. Set width of wrapperView to match RNSScreenStackHeaderSubview BUT when
268+
// RNSScreenStackHeaderSubview's width is smaller that minimal required 36 width, it breaks
269+
// UIKit's constraint. That's why we need to lower the priority of the constraint.
270+
NSLayoutConstraint *widthEqual = [wrapperView.widthAnchor constraintEqualToAnchor:self.widthAnchor];
271+
widthEqual.priority = UILayoutPriorityDefaultHigh;
272+
widthEqual.active = YES;
273+
274+
NSLayoutConstraint *heightEqual = [wrapperView.heightAnchor constraintEqualToAnchor:self.heightAnchor];
275+
heightEqual.priority = UILayoutPriorityDefaultHigh;
276+
heightEqual.active = YES;
277+
278+
// 2. Set content hugging priority for RNSScreenStackHeaderSubview.
279+
[self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
280+
[self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
281+
282+
_barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:wrapperView];
283+
} else
258284
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
285+
{
286+
_barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self];
287+
}
259288
[self configureBarButtonItem];
260289
}
261290

@@ -281,7 +310,7 @@ - (void)configureBarButtonItem
281310
[_barButtonItem setHidesSharedBackground:_hidesSharedBackground];
282311
}
283312
}
284-
#endif
313+
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
285314
}
286315

287316
- (void)setHidesSharedBackground:(BOOL)hidesSharedBackground

0 commit comments

Comments
 (0)