Skip to content

feat(Stack v5, iOS): Support icon loading in header items and menu#4277

Merged
kmichalikk merged 6 commits into
mainfrom
@kmichalikk/stack-header-items-images
Jul 10, 2026
Merged

feat(Stack v5, iOS): Support icon loading in header items and menu#4277
kmichalikk merged 6 commits into
mainfrom
@kmichalikk/stack-header-items-images

Conversation

@kmichalikk

@kmichalikk kmichalikk commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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

Description

This PR adds support for symbols and images in header items and menus. This includes sfSymbols, xcassets, imageSources and templateSources. Values can be passed via icon defining an object with contents depending on selected icon type. For image loading, RCTImageLoader is used, with protocol layer so that non-RN loader could theoretically be used, too.

Changes

  • added icon prop both for header items and menu items, with docstring. StackHeaderItem resolves require() asset sources via Image.resolveAssetSource before passing to native. Menu element icons are recursively resolved.
  • added RNSStackHeaderIconData and RNSStackHeaderIconMapper for mapping folly::dynamic dictionary to native object
  • added RNSStackHeaderIconResolver for wrapping any class that conforms to RNSImageLoading protocol, with caching
  • updated coordinators to rebuild menus and items when icon changes, renamed menuToggleCallback to menuInvalidatedCallback since it now fires for more than menu toggle

Important

Async loading images (imageSource, templateSource) means that for a brief time, icon might be missing. If we leave the field to be nil, UIKit automatically takes the item label and tries to animate it which results in a horrible glitching. To avoid this, I'm setting a blank image until the actual image finishes loading.

Important

The above blank image workaround is not complete; we should create one that has the same size as the image being loaded - this shows in menus and iOS 18 items where there is no minimal width that iOS 26 bubble has. I've created another PR for this since I'm not sure we want to proceed with it as it currently is: #4295

Before & after - visual documentation

stack-v5-icons.mov

Note

Crossfade between image source and template source doesn't work currently. I'm not sure we'll be able to make it work. It only works correctly when I set a placeholder to be a systemImageNamed, but this obviously is not possible to have, because it shows some arbitrary icon for few frames.

Test plan

Use test-stack-header-icon-ios SFT.

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

Copilot AI left a comment

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.

Pull request overview

Adds iOS Stack v5 support for rendering icons in header items and menu elements, including synchronous (SF Symbols, xcassets) and async (image/template sources) loading via an image-loader abstraction.

Changes:

  • Adds icon prop to iOS header items and menu items/submenus on the JS side, including asset-source resolution for require() images.
  • Introduces native icon data/mapping/resolution and wires async image loading via an RNSImageLoading protocol backed by RCTImageLoader.
  • Updates iOS header/menu coordinators to rebuild UI when icon loads or menu state changes; adds an iOS SFT scenario covering the new behavior.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
src/fabric/gamma/stack/StackHeaderItemIOSNativeComponent.ts Adds icon to the Fabric native component props.
src/components/gamma/stack/header/StackHeaderConfig.ios.types.ts Public iOS header config types include icon with docstring.
src/components/gamma/stack/header/ios/StackHeaderMenu.ios.types.ts Adds icon to menu item/submenu types with docs.
src/components/gamma/stack/header/ios/StackHeaderItem.ios.types.ts Adds icon to header item props.
src/components/gamma/stack/header/ios/StackHeaderItem.ios.tsx Resolves require() sources via Image.resolveAssetSource and recursively resolves menu icons.
src/components/gamma/stack/header/index.ts Re-exports PlatformIconIOS for consumers of the header API.
ios/helpers/image/RNSImageLoading.h New protocol abstraction for loading images from JSON sources.
ios/gamma/stack/screen/RNSStackScreenHeaderCoordinator.mm Passes image loader into item/menu building so icons can resolve.
ios/gamma/stack/screen/RNSStackScreenHeaderCoordinator.h Adds imageLoader property to coordinator.
ios/gamma/stack/header/RNSStackHeaderMenuMapper.mm Parses icon field for menus and menu items.
ios/gamma/stack/header/RNSStackHeaderMenuData.mm Stores icon in menu/menu-item data objects.
ios/gamma/stack/header/RNSStackHeaderMenuData.h Adds icon property and init params for menu/menu-item data.
ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.mm Resolves icons into UIMenu / UIAction images; invalidates menu on icon load.
ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.h Updates menu API to accept image loader + invalidation callback.
ios/gamma/stack/header/RNSStackHeaderItemDataProviding.h Exposes icon on header-item data provider protocol.
ios/gamma/stack/header/RNSStackHeaderItemComponentView.mm Maps JS icon prop into native icon data and triggers updates.
ios/gamma/stack/header/RNSStackHeaderItemComponentView.h Exposes icon on the component view.
ios/gamma/stack/header/RNSStackHeaderIconResolver.mm New resolver for SF Symbols / xcassets (sync) and image sources (async).
ios/gamma/stack/header/RNSStackHeaderIconResolver.h Resolver interface for icon resolution with async completion.
ios/gamma/stack/header/RNSStackHeaderIconMapper.mm New mapper from dictionary to native icon data.
ios/gamma/stack/header/RNSStackHeaderIconMapper.h Mapper interface.
ios/gamma/stack/header/RNSStackHeaderIconData.mm New icon data object implementation.
ios/gamma/stack/header/RNSStackHeaderIconData.h New icon data object interface + icon-type enum + resolved-image cache.
ios/gamma/stack/header/RNSStackHeaderContentFactory.mm Applies resolved icons to UIBarButtonItem and uses placeholder image during async loads.
ios/gamma/stack/header/RNSStackHeaderContentFactory.h Updates content-factory API to accept an image loader.
ios/gamma/stack/header/RNSStackHeaderConfigComponentView.mm Implements RNSImageLoading using RCTImageLoader retrieved from Fabric state.
ios/gamma/stack/header/RNSStackHeaderConfigComponentView.h Declares RNSImageLoading conformance.
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderConfigState.h Adds imageLoader weak pointer storage in state.
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderConfigState.cpp Implements set/getImageLoader.
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderConfigShadowNode.h Adds setImageLoader and mutable-state accessor.
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderConfigShadowNode.cpp Wires setImageLoader into state data.
common/cpp/react/renderer/components/rnscreens/RNSStackHeaderConfigComponentDescriptor.h Injects RCTImageLoader from context into shadow node state (non-Android).
apps/src/tests/single-feature-tests/stack-v5/test-stack-header-icon-ios/scenario-description.ts New SFT scenario metadata for header/menu icons.
apps/src/tests/single-feature-tests/stack-v5/test-stack-header-icon-ios/index.tsx New SFT scenario UI to cycle icon types and validate updates.
apps/src/tests/single-feature-tests/stack-v5/index.ts Registers the new iOS icon SFT scenario.

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

Comment thread src/fabric/gamma/stack/StackHeaderItemIOSNativeComponent.ts Outdated
Comment thread src/components/gamma/stack/header/ios/StackHeaderItem.ios.tsx
Comment thread ios/helpers/image/RNSImageLoading.h
Comment thread ios/gamma/stack/header/RNSStackHeaderConfigComponentView.mm
Comment thread ios/gamma/stack/header/RNSStackHeaderContentFactory.mm
Comment thread ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.mm
Comment thread ios/gamma/stack/header/RNSStackHeaderMenuCoordinator.mm
Comment thread ios/gamma/stack/header/RNSStackHeaderIconResolver.h
Comment thread ios/gamma/stack/header/RNSStackHeaderIconResolver.mm
Comment thread ios/gamma/stack/header/RNSStackHeaderIconMapper.mm
}
weakBarButtonItem.image = image;
}];
barButtonItem.image = syncImage ? syncImage : [UIImage imageWithCIImage:[CIImage emptyImage]];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was not able to think of anything better. If you remove the empty image fallback, the attached test will show that the item resizes when image loads

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 we add support for tinting, we should update the test to check that templateSource is tinted correctly (how about nested menus here?) and imageSource is not.

Comment thread src/fabric/gamma/stack/StackHeaderItemIOSNativeComponent.ts Outdated
Comment thread src/components/gamma/stack/header/index.ts Outdated
Comment thread ios/gamma/stack/header/RNSStackHeaderConfigComponentView.mm Outdated

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSInteger, RNSStackHeaderIconType) {

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 wonder if we should create a shared enum for both tabs and stack icons. But this can be refactored some other time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

that's for another time, yes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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


@protocol RNSImageLoading <NSObject>

- (void)loadImageFromJsonSource:(NSDictionary *)jsonSource

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.

This json relies on react-native and is not defined here in any way so it's not the best separation but I guess that for us it's okay.

On Android I handled image loading a bit differently, header config is fully responsible for loading images and providing Drawable (UIImage equivalent) to native impl. But I'm not sure which approach is better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think it's a "good enough" separation for now. I think I could make a better separation but would probably do it in a separate PR

Comment thread apps/src/tests/single-feature-tests/stack-v5/test-stack-header-icon-ios/index.tsx Outdated
Comment thread ios/gamma/stack/header/RNSStackHeaderIconResolver.h
Comment thread ios/gamma/stack/header/RNSStackHeaderConfigComponentView.mm
} from './StackHeaderMenu.ios.types';
import { Image, NativeSyntheticEvent, StyleSheet } from 'react-native';

function resolveIconAssetSources(

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.

This looks like a general helper, at some point, we may consider moving it to utilities and sharing across components - outside of the scope of this PR

@kmichalikk kmichalikk force-pushed the @kmichalikk/stack-header-items-images branch from d46eb35 to 5014e91 Compare July 9, 2026 12:47
@kmichalikk kmichalikk requested review from kligarski and t0maboro July 10, 2026 06:03
…4295)

## Description

This PR adds sizing calculations for placeholder image size so that
placeholders for images that are loaded asynchronously are not causing
layout shifts.

## Changes

- added a method `RNSStackHeaderIconResolver.placeholderImageForIcon`
that builds an empty image of target image's size and used for item and
menu rendering

## Before & after - visual documentation

| Before | After |
| --- | --- |
| <video
src="https://github.com/user-attachments/assets/b3b401ea-d22c-4063-9383-a1deda1b0b02"
/> | <video
src="https://github.com/user-attachments/assets/0cedf8dd-021f-4afe-a3e3-82c69e96568c"
/> |
| <video
src="https://github.com/user-attachments/assets/3d4cd395-95ae-4656-894c-abe812fa8572"
/> | <video
src="https://github.com/user-attachments/assets/58836fbc-b737-486e-8dd5-d003ddc11981"
/> |

## Test plan

Use test-stack-header-item-ios SFT

## 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 merged commit 1f38fb6 into main Jul 10, 2026
8 checks passed
@kmichalikk kmichalikk deleted the @kmichalikk/stack-header-items-images branch July 10, 2026 07:54
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.

4 participants