Skip to content

feat: Implement subviews layout in header for iOS stack v5#3868

Open
kmichalikk wants to merge 11 commits into
mainfrom
@kmichalikk/stack-header-v5-ios
Open

feat: Implement subviews layout in header for iOS stack v5#3868
kmichalikk wants to merge 11 commits into
mainfrom
@kmichalikk/stack-header-v5-ios

Conversation

@kmichalikk
Copy link
Copy Markdown
Contributor

@kmichalikk kmichalikk commented Apr 10, 2026

Closes https://github.com/software-mansion/react-native-screens-labs/issues/856

Description

This PR adds basic support for header configuration under new stack v5 on iOS. The primary focus here is to support the layout of custom react views inside the header. The following places could host a custom view:

  • leftBarButtonItems: multiple items, any of which can be react view
  • rightBarButtonItems: same as above
  • titleView: custom view to replace the regular title
  • subtitleView: to replace subview, below the title
  • largeSubtitleView: custom view below large title

They are expected to be laid out seemlessly along native elements & with shadow state position matching the native hierarchy.

The configuration flow in new implementation follows roughly this diagram:

header-config-flow

It was decided to give each item (no matter if it ends up as custom view or regular label button) a react native component. This way, each change to the props of each item maintains the same update flow: whenever props are updated, the menu item gets invalidated, which notifies the header config, which triggers the rebuild.

Caution

Optimization of the menu rebuild is not handled in this PR. For now, each invalidation triggers full menu rebuild. Same goes for any other optimization. This is an MVP implementation which will be iterated upon.

Caution

For now, no configuration is allowed for back button. Value of leftItemsSupplementBackButton is hardcoded to YES so no matter if left items are present or not, the back button is present. In fact, if the back button is not present, back gestures don't work - also tested on native app. So it's set to always be visible. DOUBLE CAUTION: default iOS gesture recognizer has this behavior. If we add a custom recognizer delegate, presence of back button won't affect the ability to swipe to pop.

Caution

HeaderConfig is not exposed outside StackContainer. Testing the header requires modifying the values directly in StackContainer. The API is not final and is subject to change any time.

Important

I hadn't really considered item menus implementation (shown by long pressing items, if configured). Menus can hold native items (custom views don't work), and can be nested. The "configure-by-native-component" flow would be awkward for such menus. We need a better API for this. I hope it to be seamless with the API proposed here. EDIT: we now have https://github.com/software-mansion/react-native-screens-labs/pull/1164

Important

Let's make sure we are okay with the proposed implementation. I want to have the same item configuration logic for toolbar in the future.

Before & after - visual documentation

header-items.mov

Test plan

For now, there are props: title, subtitle, left and right items, titleItem, subtitleItem, largeSubtitleItem, largeTitleEnabled. TitleItem should take precedence over title. The title controls both regular and large title, same for subtitle.

There are regular label buttons and custom view buttons. There are also spacers (fixed, fixed + width, flexible; for header, only fixed works on iOS 26, fixed with width works on iOS 18, flexible will work on toolbar when it will be introduced).

Use single-feature-tests/stack-v5/test-stack-subviews-ios. Setting the props should work. Mixing custom and regular buttons should work. The shadow tree position should match whatever is visible on the screen. Button in, out, click events should work.

Caution

For now, hitSlop and clicking on largeHeader subview don't work. Both should be fixed with https://github.com/software-mansion/react-native-screens-labs/issues/1417

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

@kmichalikk kmichalikk marked this pull request as draft April 10, 2026 08:49
@kmichalikk kmichalikk force-pushed the @kmichalikk/stack-header-v5-ios branch from 9f7743a to 3126537 Compare April 10, 2026 08:50
@kmichalikk kmichalikk self-assigned this Apr 10, 2026
@kmichalikk kmichalikk force-pushed the @kmichalikk/stack-v5-with-queue-ios branch from d8a04d2 to 2571b25 Compare April 13, 2026 15:34
Base automatically changed from @kmichalikk/stack-v5-with-queue-ios to main April 14, 2026 16:05
@kmichalikk kmichalikk force-pushed the @kmichalikk/stack-header-v5-ios branch from 3126537 to 47620e6 Compare May 4, 2026 10:29
@kmichalikk kmichalikk force-pushed the @kmichalikk/stack-header-v5-ios branch from 5acc30e to 19fb657 Compare May 5, 2026 09:56
@kmichalikk kmichalikk marked this pull request as ready for review May 5, 2026 10:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an MVP iOS implementation for Stack v5 header configuration with support for laying out React Native-backed header items (left/right/title/subtitle/large subtitle) and synchronizing their shadow state frames with UIKit navigation bar layout.

Changes:

  • Introduces new Fabric native components for iOS header config + header items/spacers, plus corresponding iOS coordinators to apply configuration to UINavigationItem/UINavigationBar.
  • Adds C++ shadow-node/state plumbing to correct header/header-item layout origins based on native-measured frames.
  • Adds a dedicated single-feature test scenario for iOS header subviews (and updates scenario registration/naming).

Reviewed changes

Copilot reviewed 52 out of 52 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/fabric/gamma/stack/StackHeaderItemSpacerIOSNativeComponent.ts Codegen spec for iOS header spacers
src/fabric/gamma/stack/StackHeaderItemIOSNativeComponent.ts Codegen spec for iOS header items
src/fabric/gamma/stack/StackHeaderConfigIOSNativeComponent.ts Adds subtitle/largeTitle props to iOS header config
src/components/gamma/stack/header/StackHeaderConfig.types.ts Adds subtitle to shared header config props
src/components/gamma/stack/header/StackHeaderConfig.ios.types.ts Defines iOS header item/spacer config types
src/components/gamma/stack/header/StackHeaderConfig.ios.tsx Renders iOS header config children (items/spacers)
src/components/gamma/stack/header/ios/StackHeaderItemSpacer.ios.types.ts JS spacer wrapper prop types
src/components/gamma/stack/header/ios/StackHeaderItemSpacer.ios.tsx JS spacer wrapper component
src/components/gamma/stack/header/ios/StackHeaderItem.ios.types.ts JS header item wrapper prop types
src/components/gamma/stack/header/ios/StackHeaderItem.ios.tsx JS header item wrapper component
package.json Registers new Fabric component view class mappings
ios/gamma/stack/screen/RNSStackScreenHeaderCoordinator.mm New coordinator to apply header data + shadow sync
ios/gamma/stack/screen/RNSStackScreenHeaderCoordinator.h Coordinator interface
ios/gamma/stack/screen/RNSStackScreenController.mm Creates/uses header coordinator in lifecycle
ios/gamma/stack/screen/RNSStackScreenController.h Exposes headerCoordinator property
ios/gamma/stack/screen/RNSStackScreenComponentView.mm Adds header config lookup helper
ios/gamma/stack/screen/RNSStackScreenComponentView.h Declares header config lookup helper
ios/gamma/stack/screen/RNSStackNavigationItemCoordinator.mm Applies header data to UINavigationItem
ios/gamma/stack/screen/RNSStackNavigationItemCoordinator.h Navigation item coordinator interface
ios/gamma/stack/RNSStackNavigationBarCoordinator.mm Applies bar-level config to UINavigationController
ios/gamma/stack/RNSStackNavigationBarCoordinator.h Navigation bar coordinator interface
ios/gamma/stack/RNSShadowStateFrameTracker.mm Utility to debounce frame-based state updates
ios/gamma/stack/RNSShadowStateFrameTracker.h Utility header
ios/gamma/stack/host/RNSStackNavigationController.mm Dummy VC workaround + shadow-state updates on layout
ios/gamma/stack/host/RNSStackHostComponentView.mm Auto-layout constraints for embedded controller view
ios/gamma/stack/header/RNSStackHeaderItemSpacerComponentView.mm Fabric component view for header spacers
ios/gamma/stack/header/RNSStackHeaderItemSpacerComponentView.h Spacer component view interface
ios/gamma/stack/header/RNSStackHeaderItemInvalidationDelegate.h Delegate for header item invalidation
ios/gamma/stack/header/RNSStackHeaderItemComponentView.mm Fabric component view for header items
ios/gamma/stack/header/RNSStackHeaderItemComponentView.h Header item component view interface
ios/gamma/stack/header/RNSStackHeaderData.mm Immutable header data container impl
ios/gamma/stack/header/RNSStackHeaderData.h Immutable header data container interface
ios/gamma/stack/header/RNSStackHeaderConfigComponentView.mm Fabric component view building header data/items
ios/gamma/stack/header/RNSStackHeaderConfigComponentView.h Header config component view interface
ios/gamma/stack/header/RNSHeaderItemSpacerPlacement.h Native enum for spacer placement
ios/gamma/stack/header/RNSHeaderItemPlacement.h Native enum for item placement
ios/conversion/RNSConversions-Stack.mm Adds enum conversions for header placements
ios/conversion/RNSConversions-Stack.h Declares new conversion templates
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderItemState.h New state type for header item frame/offset
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderItemState.cpp State TU
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderItemShadowNode.h Shadow node that applies native frame corrections
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderItemShadowNode.cpp Shadow node layout correction implementation
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderItemComponentDescriptor.h Component descriptor for header items
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderConfigState.h Extends state to carry size/offset on non-Android
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderConfigShadowNode.h Adds non-Android layout override
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderConfigShadowNode.cpp Applies origin corrections from state on iOS
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderConfigComponentDescriptor.h Makes adopt logic cross-platform
apps/src/tests/single-feature-tests/stack-v5/test-stack-subviews-ios/scenario.md Adds iOS header subviews scenario doc
apps/src/tests/single-feature-tests/stack-v5/test-stack-subviews-ios/index.tsx Adds iOS interactive scenario implementation
apps/src/tests/single-feature-tests/stack-v5/test-stack-subviews-android/index.tsx Renames Android scenario label
apps/src/tests/single-feature-tests/stack-v5/index.ts Registers iOS + Android subviews scenarios separately
apps/src/shared/gamma/containers/stack/StackContainer.tsx Formatting-only change in stack mapping destructure

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/gamma/stack/header/ios/StackHeaderItemSpacer.ios.tsx
Comment thread src/components/gamma/stack/header/StackHeaderConfig.ios.tsx
Comment thread src/components/gamma/stack/header/StackHeaderConfig.ios.types.ts
Comment thread ios/gamma/stack/header/RNSStackHeaderItemSpacerComponentView.mm
Comment thread ios/gamma/stack/header/RNSStackHeaderItemSpacerComponentView.mm
Comment thread ios/gamma/stack/header/RNSStackHeaderConfigComponentView.mm
Comment thread ios/gamma/stack/screen/RNSStackNavigationItemCoordinator.mm
Comment thread ios/gamma/stack/host/RNSStackNavigationController.mm
Copy link
Copy Markdown
Contributor

@kligarski kligarski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Pressables don't work on first load.
pressables_bug.mov

  1. Aren't we missing RNSGammaStubs?

- custom view buttons don't move to overflow menu (native behavior); one needs to specify `menuRepresentation` for them (not implemented yet)
- items are collapsed in the order: title, right buttons (one by one), left buttons (all at once); this is in line with native behavior

## Steps
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add some TODO here

Comment on lines +11 to +12
## E2E test

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's follow convention here - add OTHER and explanation that it's still todo.

return (
<PressableWithFeedback
{...pressableProps}
style={{ width: 30, height: 30, backgroundColor: 'blue' }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When testing pressables, I usually depend on its color changing when clicked. If we override the color here, PressableWithFeedback loses its WithFeedback functionality. I think we should stick to default colors here, maybe we can open separate PR that will allow color customization for PressableWithFeedback component.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're really missing some configuration here, e.g. I can't test titleView without going to landscape mode or modifying the code of the test. There should be some configuration of left/right subviews I think, even something like enable/disable.

title?: string | undefined;
/**
* @summary Subtitle displayed in the header.
*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a comment that it's currently unsupported on Android (but it will be, hopefully).


@implementation RNSStackNavigationItemCoordinator

#pragma mark - Public
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: not sure if we need this

Comment on lines 14 to +32
Point RNSStackHeaderConfigShadowNode::getContentOriginOffset(
bool /*includeTransform*/) const {
auto stateData = getStateData();
return stateData.contentOffset;
}
#endif // ANDROID
#else // ANDROID
void RNSStackHeaderConfigShadowNode::layout(LayoutContext layoutContext) {
YogaLayoutableShadowNode::layout(layoutContext);
applyFrameCorrections();
}

void RNSStackHeaderConfigShadowNode::applyFrameCorrections() {
ensureUnsealed();

const auto &stateData = getStateData();
layoutMetrics_.frame.origin.x = stateData.contentOffset.x;
layoutMetrics_.frame.origin.y = stateData.contentOffset.y;
}
#endif
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to modify layoutMetrics_.frame.origin here instead of relying on getContentOriginOffset? Not sure if there is much difference tbh? cc @kkafar here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar question regarding header items.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe that's better because to handle RTL we need it on Android anyway for header subviews? I'm not sure.

Comment on lines +45 to +46
const Size frameSize{};
const Point contentOffset{};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to move this to shared part between Android and iOS.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we even need this file?


- (RNSStackScreenController *)requireScreenController
{
RCTAssert(_screenController != nil, @"Screen Controller cannot be nil");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
RCTAssert(_screenController != nil, @"Screen Controller cannot be nil");
RCTAssert(_screenController != nil, @"[RNScreens] Screen Controller cannot be nil");

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants