Skip to content

Commit da6c4d7

Browse files
t0maborokkafar
andauthored
fix(iOS, Tabs): Request layout pass after loading image from source (#3596)
## Description Before assigning the image, the tab bar might already be attached to the view hierarchy. This can cause an issue where the tab bar layout is not correctly updated, as it might have been added to the window before the image was set. As a result, the layout adjustments (e.g., image positioning) are not applied immediately. To resolve this, we explicitly request an additional layout pass after the image is assigned. This ensures that the tab bar updates its layout properly without requiring user interaction (e.g., switching tabs) to trigger a recalculation. The issue is illustrated in the image below. <img width="405" height="848" alt="before-image" src="https://github.com/user-attachments/assets/a1f45277-86b5-491b-a31b-5e146ee5a782" /> ## Changes - Add layout request in the completion callback when the image is loaded from source. - Add example & assets demonstrating the issue. ## Before & after - visual documentation | Before | After | | --- | --- | | <video src="https://github.com/user-attachments/assets/bcaedb0f-ded8-482e-9402-7c178c50773c" /> | <video src="https://github.com/user-attachments/assets/7e8dd72a-e496-4af4-9606-50cc5b9a1277" /> | ## Test plan Added Test3596, performed regression testing on TestBottomTabs ## Checklist - [x] Included code example that can be used to test this change. - [ ] Updated / created local changelog entries in relevant test files. - [x] For visual changes, included screenshots / GIFs / recordings documenting the change. - [ ] For API changes, updated relevant public types. - [x] Ensured that CI passes --------- Co-authored-by: Kacper Kafara <kacper.kafara@swmansion.com>
1 parent 0d88498 commit da6c4d7

6 files changed

Lines changed: 115 additions & 2 deletions

File tree

3.77 KB
Loading
20.5 KB
Loading
35.1 KB
Loading
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from 'react';
2+
3+
import { enableFreeze } from 'react-native-screens';
4+
import ConfigWrapperContext, {
5+
type Configuration,
6+
DEFAULT_GLOBAL_CONFIGURATION,
7+
} from '../../shared/gamma/containers/bottom-tabs/ConfigWrapperContext';
8+
import {
9+
BottomTabsContainer,
10+
type TabConfiguration,
11+
} from '../../shared/gamma/containers/bottom-tabs/BottomTabsContainer';
12+
import { CenteredLayoutView } from '../../shared/CenteredLayoutView';
13+
import { Text } from 'react-native';
14+
import Colors from '../../shared/styling/Colors';
15+
16+
enableFreeze(true);
17+
18+
const ICON = require('../../../assets/variableIcons/globe_oversized.png');
19+
20+
function makeTab(title: string) {
21+
return function Tab() {
22+
return (
23+
<CenteredLayoutView style={{backgroundColor: Colors.PurpleLight60}}>
24+
<Text>{title}</Text>
25+
</CenteredLayoutView>
26+
);
27+
};
28+
}
29+
30+
const TAB_CONFIGS: TabConfiguration[] = [
31+
{
32+
tabScreenProps: {
33+
tabKey: 'Tab1',
34+
title: 'Tab 1',
35+
icon: {
36+
shared: {
37+
type: 'imageSource',
38+
imageSource: ICON,
39+
},
40+
},
41+
},
42+
component: makeTab('Tab 1'),
43+
},
44+
{
45+
tabScreenProps: {
46+
tabKey: 'Tab2',
47+
title: 'Tab 2',
48+
icon: {
49+
shared: {
50+
type: 'imageSource',
51+
imageSource: ICON,
52+
},
53+
},
54+
},
55+
component: makeTab('Tab 2'),
56+
},
57+
{
58+
tabScreenProps: {
59+
tabKey: 'Tab3',
60+
title: 'Tab 3',
61+
systemItem: 'search',
62+
},
63+
component: makeTab('Tab 3'),
64+
},
65+
];
66+
67+
function App() {
68+
const [config, setConfig] = React.useState<Configuration>(
69+
DEFAULT_GLOBAL_CONFIGURATION,
70+
);
71+
72+
return (
73+
<ConfigWrapperContext.Provider
74+
value={{
75+
config,
76+
setConfig,
77+
}}>
78+
<BottomTabsContainer tabConfigs={TAB_CONFIGS} />
79+
</ConfigWrapperContext.Provider>
80+
);
81+
}
82+
83+
export default App;

apps/src/tests/issue-tests/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export { default as Test3522 } from './Test3522';
179179
export { default as Test3564 } from './Test3564';
180180
export { default as Test3566 } from './Test3566';
181181
export { default as Test3576 } from './Test3576';
182+
export { default as Test3596 } from './Test3596';
182183
export { default as TestScreenAnimation } from './TestScreenAnimation';
183184
// The following test was meant to demo the "go back" gesture using Reanimated
184185
// but the associated PR in react-navigation is currently put on hold

ios/bottom-tabs/RNSTabBarAppearanceCoordinator.mm

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ - (void)setIconsForTabBarItem:(UITabBarItem *)tabBarItem
9797
withImageLoader:imageLoader
9898
asTemplate:isTemplate
9999
completionBlock:^(UIImage *image) {
100-
tabBarItem.image = image;
100+
[self updateTabBarItem:tabBarItem
101+
withImage:image
102+
isSelected:NO
103+
forScreenView:screenView];
101104
}];
102105
} else {
103106
tabBarItem.image = nil;
@@ -109,7 +112,10 @@ - (void)setIconsForTabBarItem:(UITabBarItem *)tabBarItem
109112
withImageLoader:imageLoader
110113
asTemplate:isTemplate
111114
completionBlock:^(UIImage *image) {
112-
tabBarItem.selectedImage = image;
115+
[self updateTabBarItem:tabBarItem
116+
withImage:image
117+
isSelected:YES
118+
forScreenView:screenView];
113119
}];
114120
} else {
115121
tabBarItem.selectedImage = nil;
@@ -119,6 +125,29 @@ - (void)setIconsForTabBarItem:(UITabBarItem *)tabBarItem
119125
}
120126
}
121127

128+
- (void)updateTabBarItem:(UITabBarItem *)tabBarItem
129+
withImage:(UIImage *)image
130+
isSelected:(BOOL)isSelected
131+
forScreenView:(RNSBottomTabsScreenComponentView *)screenView
132+
{
133+
if (isSelected) {
134+
tabBarItem.selectedImage = image;
135+
} else {
136+
tabBarItem.image = image;
137+
}
138+
139+
// A layout pass is required because the image might be loaded asynchronously,
140+
// after the tab bar has already been attached to the window.
141+
// This code handles case where image passed by the user is not
142+
// of appropriate size & needs to be readjusted. W/o additional
143+
// layout here the icon would be displayed with original dimensions.
144+
UIViewController *parent = screenView.controller.parentViewController;
145+
if ([parent isKindOfClass:[UITabBarController class]]) {
146+
UITabBarController *tabBarVC = (UITabBarController *)parent;
147+
[tabBarVC.tabBar setNeedsLayout];
148+
}
149+
}
150+
122151
+ (void)configureTabBarAppearance:(nonnull UITabBarAppearance *)tabBarAppearance
123152
fromAppearanceProps:(nonnull NSDictionary *)appearanceProps
124153
{

0 commit comments

Comments
 (0)