From d16e0bd727177066027cd77ad6c26db86df9c711 Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Wed, 17 Dec 2025 10:06:18 +0100 Subject: [PATCH 1/2] fix center subview in header --- ios/RNSScreenStackHeaderSubview.mm | 109 +++++++++++++++++------------ 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/ios/RNSScreenStackHeaderSubview.mm b/ios/RNSScreenStackHeaderSubview.mm index 869a8a2bab..24b07e52ec 100644 --- a/ios/RNSScreenStackHeaderSubview.mm +++ b/ios/RNSScreenStackHeaderSubview.mm @@ -174,14 +174,22 @@ - (void)updateLayoutMetrics:(const react::LayoutMetrics &)layoutMetrics self); } else { #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) - BOOL sizeHasChanged = _layoutMetrics.frame.size != layoutMetrics.frame.size; - _layoutMetrics = layoutMetrics; - if (sizeHasChanged) { - [self invalidateIntrinsicContentSize]; + BOOL needsAutoLayout = NO; + if (@available(iOS 26.0, *)) { + needsAutoLayout = _type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight; } -#else // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) - self.bounds = CGRect{CGPointZero, frame.size}; + + if (needsAutoLayout) { + BOOL sizeHasChanged = _layoutMetrics.frame.size != layoutMetrics.frame.size; + _layoutMetrics = layoutMetrics; + if (sizeHasChanged) { + [self invalidateIntrinsicContentSize]; + } + } else #endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) + { + self.bounds = CGRect{CGPointZero, frame.size}; + } [self layoutNavigationBar]; } } @@ -206,11 +214,19 @@ - (void)reactSetFrame:(CGRect)frame // makes UINavigationBar the only one to control the position of header content. if (!CGSizeEqualToSize(frame.size, self.frame.size)) { #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) - _lastReactFrameSize = frame.size; - [self invalidateIntrinsicContentSize]; -#else // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) - [super reactSetFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; + BOOL needsAutoLayout = NO; + if (@available(iOS 26.0, *)) { + needsAutoLayout = _type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight; + } + + if (needsAutoLayout) { + _lastReactFrameSize = frame.size; + [self invalidateIntrinsicContentSize]; + } else #endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) + { + [super reactSetFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; + } [self layoutNavigationBar]; } } @@ -221,41 +237,48 @@ - (void)reactSetFrame:(CGRect)frame - (UIBarButtonItem *)getUIBarButtonItem { + RCTAssert( + _type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight, + @"[RNScreens] Unexpected subview type."); + if (_barButtonItem == nil) { #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) - // Starting from iOS 26, UIBarButtonItem's customView is streched to have at least 36 width. - // Stretching RNSScreenStackHeaderSubview means that its subviews are aligned to left instead - // of the center. To mitigate this, we add a wrapper view that will center - // RNSScreenStackHeaderSubview inside of itself. - UIView *wrapperView = [UIView new]; - wrapperView.translatesAutoresizingMaskIntoConstraints = NO; - - self.translatesAutoresizingMaskIntoConstraints = NO; - [wrapperView addSubview:self]; - - [self.centerXAnchor constraintEqualToAnchor:wrapperView.centerXAnchor].active = YES; - [self.centerYAnchor constraintEqualToAnchor:wrapperView.centerYAnchor].active = YES; - - // To prevent UIKit from stretching subviews to all available width, we need to: - // 1. Set width of wrapperView to match RNSScreenStackHeaderSubview BUT when - // RNSScreenStackHeaderSubview's width is smaller that minimal required 36 width, it breaks - // UIKit's constraint. That's why we need to lower the priority of the constraint. - NSLayoutConstraint *widthEqual = [wrapperView.widthAnchor constraintEqualToAnchor:self.widthAnchor]; - widthEqual.priority = UILayoutPriorityDefaultHigh; - widthEqual.active = YES; - - NSLayoutConstraint *heightEqual = [wrapperView.heightAnchor constraintEqualToAnchor:self.heightAnchor]; - heightEqual.priority = UILayoutPriorityDefaultHigh; - heightEqual.active = YES; - - // 2. Set content hugging priority for RNSScreenStackHeaderSubview. - [self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal]; - [self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; - - _barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:wrapperView]; -#else // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) - _barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self]; + if (@available(iOS 26.0, *)) { + // Starting from iOS 26, UIBarButtonItem's customView is streched to have at least 36 width. + // Stretching RNSScreenStackHeaderSubview means that its subviews are aligned to left instead + // of the center. To mitigate this, we add a wrapper view that will center + // RNSScreenStackHeaderSubview inside of itself. + UIView *wrapperView = [UIView new]; + wrapperView.translatesAutoresizingMaskIntoConstraints = NO; + + self.translatesAutoresizingMaskIntoConstraints = NO; + [wrapperView addSubview:self]; + + [self.centerXAnchor constraintEqualToAnchor:wrapperView.centerXAnchor].active = YES; + [self.centerYAnchor constraintEqualToAnchor:wrapperView.centerYAnchor].active = YES; + + // To prevent UIKit from stretching subviews to all available width, we need to: + // 1. Set width of wrapperView to match RNSScreenStackHeaderSubview BUT when + // RNSScreenStackHeaderSubview's width is smaller that minimal required 36 width, it breaks + // UIKit's constraint. That's why we need to lower the priority of the constraint. + NSLayoutConstraint *widthEqual = [wrapperView.widthAnchor constraintEqualToAnchor:self.widthAnchor]; + widthEqual.priority = UILayoutPriorityDefaultHigh; + widthEqual.active = YES; + + NSLayoutConstraint *heightEqual = [wrapperView.heightAnchor constraintEqualToAnchor:self.heightAnchor]; + heightEqual.priority = UILayoutPriorityDefaultHigh; + heightEqual.active = YES; + + // 2. Set content hugging priority for RNSScreenStackHeaderSubview. + [self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal]; + [self setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; + + _barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:wrapperView]; + } else #endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) + { + _barButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self]; + } [self configureBarButtonItem]; } @@ -281,7 +304,7 @@ - (void)configureBarButtonItem [_barButtonItem setHidesSharedBackground:_hidesSharedBackground]; } } -#endif +#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) } - (void)setHidesSharedBackground:(BOOL)hidesSharedBackground From 9167b9140aa4e26dae99e6d97496f5c4cb58db59 Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Wed, 17 Dec 2025 12:49:11 +0100 Subject: [PATCH 2/2] extract needsAutoLayout --- ios/RNSScreenStackHeaderSubview.mm | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/ios/RNSScreenStackHeaderSubview.mm b/ios/RNSScreenStackHeaderSubview.mm index 24b07e52ec..b97db0d9e2 100644 --- a/ios/RNSScreenStackHeaderSubview.mm +++ b/ios/RNSScreenStackHeaderSubview.mm @@ -174,12 +174,7 @@ - (void)updateLayoutMetrics:(const react::LayoutMetrics &)layoutMetrics self); } else { #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) - BOOL needsAutoLayout = NO; - if (@available(iOS 26.0, *)) { - needsAutoLayout = _type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight; - } - - if (needsAutoLayout) { + if (self.needsAutoLayout) { BOOL sizeHasChanged = _layoutMetrics.frame.size != layoutMetrics.frame.size; _layoutMetrics = layoutMetrics; if (sizeHasChanged) { @@ -214,12 +209,7 @@ - (void)reactSetFrame:(CGRect)frame // makes UINavigationBar the only one to control the position of header content. if (!CGSizeEqualToSize(frame.size, self.frame.size)) { #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) - BOOL needsAutoLayout = NO; - if (@available(iOS 26.0, *)) { - needsAutoLayout = _type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight; - } - - if (needsAutoLayout) { + if (self.needsAutoLayout) { _lastReactFrameSize = frame.size; [self invalidateIntrinsicContentSize]; } else @@ -233,6 +223,22 @@ - (void)reactSetFrame:(CGRect)frame #endif // RCT_NEW_ARCH_ENABLED +#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) + +// Starting from iOS 26, to center left and right subviews inside liquid glass backdrop, +// we need to use auto layout. To make Yoga's layout work with auto layout, we pass information +// from Yoga via `intrinsicContentSize`. +- (BOOL)needsAutoLayout +{ + BOOL needsAutoLayout = NO; + if (@available(iOS 26.0, *)) { + needsAutoLayout = _type == RNSScreenStackHeaderSubviewTypeLeft || _type == RNSScreenStackHeaderSubviewTypeRight; + } + return needsAutoLayout; +} + +#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) + #pragma mark - UIBarButtonItem specific - (UIBarButtonItem *)getUIBarButtonItem