Skip to content

Commit 3c4ef70

Browse files
committed
feat(bottom-tabs,iOS): add support for badgeColor (#9)
## Description Add support for badge colour customisation on particular tab screens. Currently this is implemented using `appearance` objects, therefore selected tab bar item imposes its style for all other tab bar items. Note that effect of individual badge color for each tab bar item can be achieved with `tabBarItem.badgeColor`, however this seems to disable `appearance` mechanism, therefore I didn't go that way. ## Changes TabBar appearance updates are now coordinated by `RNSTabBarAppearanceCoordinator` & done when requested on `RNSTabBarController`. This PR also changes semantic of `RNSTabBarController._pendingChildren` field: 1. it got renamed to `tabScreenControllers`, 2. it is no longer nulled - now it just keeps last children received from host component. This was needed, because configuring the appearance required access to all child controllers, which `RNSTabBarController.viewControllers` does tot provide (in case there are more than 5/6 controllers, surplus ones are put into `MoreViewController` & not returned by `RNSTabBarController.viewControllers` call. ## Test code and steps to reproduce `TestBottomTabs` ## Checklist - [x] Included code example that can be used to test this change - [ ] Ensured that CI passes
1 parent 436d430 commit 3c4ef70

15 files changed

+319
-87
lines changed

apps/src/tests/TestBottomTabs.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { Button, Text, View, ViewProps } from 'react-native';
33

44
import { BottomTabs, BottomTabsScreen, enableFreeze } from 'react-native-screens';
5+
import Colors from '../shared/styling/Colors';
56

67
enableFreeze(true);
78

@@ -31,23 +32,23 @@ function App() {
3132

3233
return (
3334
<View style={{ flex: 1 }}>
34-
<BottomTabs tabBarBackgroundColor={'rgba(255, 255, 0, 0.5)'} tabBarBlurEffect={'dark'}>
35-
<BottomTabsScreen isFocused={focusedTab % 3 === 0} badgeValue="Tab 1">
36-
<LayoutView style={{ backgroundColor: 'lightgreen' }} tabID={0}>
35+
<BottomTabs tabBarBackgroundColor={Colors.NavyLight100}>
36+
<BottomTabsScreen isFocused={focusedTab % 3 === 0} badgeValue="Tab 1" badgeColor={Colors.NavyDark80}>
37+
<LayoutView style={{ backgroundColor: Colors.OffWhite }} tabID={0}>
3738
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
3839
<Text>Hello world from native bottom tab</Text>
3940
<Button title="Next tab" onPress={selectNextTab} />
4041
</View>
4142
</LayoutView>
4243
</BottomTabsScreen>
43-
<BottomTabsScreen isFocused={focusedTab % 3 === 1} badgeValue="Tab 2">
44-
<LayoutView style={{ backgroundColor: 'lightblue' }} tabID={1}>
44+
<BottomTabsScreen isFocused={focusedTab % 3 === 1} badgeValue="Tab 2" badgeColor={Colors.PurpleLight100}>
45+
<LayoutView style={{ backgroundColor: Colors.PurpleLight80 }} tabID={1}>
4546
<Text>Tab2 world from native bottom tab</Text>
4647
<Button title="Next tab" onPress={selectNextTab} />
4748
</LayoutView>
4849
</BottomTabsScreen>
49-
<BottomTabsScreen isFocused={focusedTab % 3 === 2} badgeValue="Tab 3">
50-
<LayoutView style={{ backgroundColor: 'yellow' }} tabID={2}>
50+
<BottomTabsScreen isFocused={focusedTab % 3 === 2} badgeValue="Tab 3" badgeColor={Colors.YellowDark120}>
51+
<LayoutView style={{ backgroundColor: Colors.YellowDark80 }} tabID={2}>
5152
<Text>Tab3 world from native bottom tab</Text>
5253
<Button title="Next tab" onPress={selectNextTab} />
5354
</LayoutView>

ios/bottom-tabs/RNSBottomTabsHostComponentView.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
2222
@interface RNSBottomTabsHostComponentView ()
2323

2424
@property (nonatomic, strong, readonly, nullable) UIColor *tabBarBackgroundColor;
25-
@property (nonatomic, readonly) RNSBlurEffectStyle tabBarBlurEffect;
25+
@property (nonatomic, strong, readonly, nullable) UIBlurEffect *tabBarBlurEffect;
2626

2727
@end
2828

ios/bottom-tabs/RNSBottomTabsHostComponentView.mm

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,28 +40,15 @@ - (void)initState
4040
{
4141
static const auto defaultProps = std::make_shared<const react::RNSBottomTabsProps>();
4242
_props = defaultProps;
43+
_tabBarBlurEffect = nil;
44+
4345
_controller = [[RNSTabBarController alloc] init];
4446
_reactSubviews = [NSMutableArray new];
47+
4548
_hasModifiedReactSubviewsInCurrentTransaction = NO;
4649
_needsTabBarAppearanceUpdate = NO;
4750
}
4851

49-
- (nullable UITabBarAppearance *)makeTabBarAppearance
50-
{
51-
UITabBarAppearance *newAppearance = [[UITabBarAppearance alloc] init];
52-
53-
newAppearance.backgroundColor = _tabBarBackgroundColor;
54-
55-
if (_tabBarBlurEffect != RNSBlurEffectStyleNone) {
56-
newAppearance.backgroundEffect =
57-
[UIBlurEffect effectWithStyle:[RNSConvert tryConvertRNSBlurEffectStyleToUIBlurEffectStyle:_tabBarBlurEffect]];
58-
} else {
59-
newAppearance.backgroundEffect = nil;
60-
}
61-
62-
return newAppearance;
63-
}
64-
6552
#pragma mark - UIView methods
6653

6754
- (void)didMoveToWindow
@@ -101,12 +88,6 @@ - (void)updateContainer
10188
NSLog(@"updateContainer: tabControllers: %@", tabControllers);
10289

10390
[_controller childViewControllersHaveChangedTo:tabControllers];
104-
105-
[[_controller tabBar] setItemPositioning:UITabBarItemPositioningCentered];
106-
NSLog(@"updateContainer: tabBarItems %@", [[_controller tabBar] items]);
107-
for (UITabBarItem *tabBarItem in [[_controller tabBar] items]) {
108-
tabBarItem.badgeValue = @"Hello!";
109-
}
11091
}
11192

11293
- (void)markChildUpdated
@@ -158,7 +139,7 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props
158139
if (newComponentProps.tabBarBlurEffect != oldComponentProps.tabBarBlurEffect) {
159140
_needsTabBarAppearanceUpdate = YES;
160141
_tabBarBlurEffect =
161-
rnscreens::conversion::RNSBlurEffectStyleFromRNSBottomTabsTabBarBlurEffect(newComponentProps.tabBarBlurEffect);
142+
rnscreens::conversion::RNSUIBlurEffectFromRNSBottomTabsTabBarBlurEffect(newComponentProps.tabBarBlurEffect);
162143
}
163144

164145
// Super call updates _props pointer. We should NOT update it before calling super.
@@ -169,7 +150,7 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
169150
{
170151
if (_needsTabBarAppearanceUpdate) {
171152
_needsTabBarAppearanceUpdate = NO;
172-
[_controller applyTabBarAppearance:[self makeTabBarAppearance]];
153+
[_controller setNeedsUpdateOfTabBarAppearance:true];
173154
}
174155
[super finalizeUpdates:updateMask];
175156
}

ios/bottom-tabs/RNSBottomTabsScreenComponentView.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ NS_ASSUME_NONNULL_BEGIN
3333

3434
@property (nonatomic, readonly) BOOL isFocused;
3535
@property (nonatomic, readonly, nullable) NSString *badgeValue;
36+
@property (nonatomic, readonly, nullable) UIColor *badgeColor;
3637

3738
@end
3839

ios/bottom-tabs/RNSBottomTabsScreenComponentView.mm

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props
111111
_controller.tabBarItem.badgeValue = _badgeValue;
112112
}
113113

114+
if (newComponentProps.badgeColor != oldComponentProps.badgeColor) {
115+
_badgeColor = RCTUIColorFromSharedColor(newComponentProps.badgeColor);
116+
// Note that this will prevent default color from being set.
117+
// TODO: support default color by setting nil here.
118+
NSLog(@"TabsScreen [%ld] update badgeColor to %@", self.tag, _badgeColor);
119+
[_controller tabItemAppearanceHasChanged];
120+
}
121+
114122
[super updateProps:props oldProps:oldProps];
115123
}
116124

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#import <Foundation/Foundation.h>
2+
#import "RNSBottomTabsHostComponentView.h"
3+
#import "RNSTabsScreenViewController.h"
4+
5+
NS_ASSUME_NONNULL_BEGIN
6+
7+
/**
8+
* Responsible for creating & applying appearance to the tab bar.
9+
*
10+
* It does take into account all properties from host component view & tab screen controllers related to tab bar
11+
* appearance and applies them accordingly in correct order.
12+
*/
13+
@interface RNSTabBarAppearanceCoordinator : NSObject
14+
15+
/**
16+
* Constructs the tab bar appearance from the ground up, basing on information contained in provided params (mostly
17+
* react props), and then applies it to the tab bar and respective tab bar items.
18+
*
19+
* Current implementation configures all tab bar styles & state (stacked, inline, normal, focused, selected, disabled,
20+
* etc.) with the same appearance.
21+
*
22+
* TODO: Do not take references to component view & controllers here. Put the tab bar appearance properites in single
23+
* type & only take it here.
24+
*/
25+
- (void)updateAppearanceOfTabBar:(nullable UITabBar *)tabBar
26+
withHostComponentView:(nullable RNSBottomTabsHostComponentView *)hostComponentView
27+
tabScreenControllers:(nullable NSArray<RNSTabsScreenViewController *> *)tabScreenCtrls;
28+
29+
@end
30+
31+
NS_ASSUME_NONNULL_END
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#import "RNSTabBarAppearanceCoordinator.h"
2+
#import "RNSConversions.h"
3+
#import "RNSTabsScreenViewController.h"
4+
5+
@implementation RNSTabBarAppearanceCoordinator
6+
7+
- (void)updateAppearanceOfTabBar:(nullable UITabBar *)tabBar
8+
withHostComponentView:(nullable RNSBottomTabsHostComponentView *)hostComponentView
9+
tabScreenControllers:(nullable NSArray<RNSTabsScreenViewController *> *)tabScreenCtrls
10+
{
11+
if (tabBar == nil) {
12+
return;
13+
}
14+
15+
// Step 1 - start with default appearance
16+
UITabBarAppearance *appearance = [[UITabBarAppearance alloc] init];
17+
18+
// Step 2 - general settings
19+
if (hostComponentView != nil) {
20+
appearance.backgroundColor = hostComponentView.tabBarBackgroundColor;
21+
appearance.backgroundEffect = hostComponentView.tabBarBlurEffect;
22+
}
23+
24+
// Step 3 - apply general settings to the tab bar
25+
tabBar.standardAppearance = appearance;
26+
tabBar.scrollEdgeAppearance = appearance;
27+
28+
// Step 4 - build the appearance object for each tab & apply it
29+
if (tabScreenCtrls == nil) {
30+
return;
31+
}
32+
33+
for (RNSTabsScreenViewController *tabScreenCtrl in tabScreenCtrls) {
34+
if (tabScreenCtrl == nil) {
35+
// It should not be null here, something went wrong.
36+
RCTLogWarn(@"[RNScreens] Nullish controller of TabScreen while tab bar appearance update!");
37+
continue;
38+
}
39+
40+
UITabBarAppearance *tabAppearance = [[UITabBarAppearance alloc] initWithBarAppearance:appearance];
41+
42+
[self configureTabBarItemAppearance:tabAppearance.compactInlineLayoutAppearance
43+
forTabScreenController:tabScreenCtrl];
44+
[self configureTabBarItemAppearance:tabAppearance.inlineLayoutAppearance forTabScreenController:tabScreenCtrl];
45+
[self configureTabBarItemAppearance:tabAppearance.stackedLayoutAppearance forTabScreenController:tabScreenCtrl];
46+
47+
tabScreenCtrl.tabBarItem.standardAppearance = tabAppearance;
48+
tabScreenCtrl.tabBarItem.scrollEdgeAppearance = tabAppearance;
49+
}
50+
}
51+
52+
- (void)configureTabBarItemAppearance:(nonnull UITabBarItemAppearance *)tabBarItemAppearance
53+
forTabScreenController:(nonnull RNSTabsScreenViewController *)tabScreenCtrl
54+
{
55+
tabBarItemAppearance.normal.badgeBackgroundColor = tabScreenCtrl.tabScreenComponentView.badgeColor;
56+
57+
tabBarItemAppearance.selected.badgeBackgroundColor = tabScreenCtrl.tabScreenComponentView.badgeColor;
58+
59+
tabBarItemAppearance.focused.badgeBackgroundColor = tabScreenCtrl.tabScreenComponentView.badgeColor;
60+
61+
tabBarItemAppearance.disabled.badgeBackgroundColor = tabScreenCtrl.tabScreenComponentView.badgeColor;
62+
}
63+
64+
@end

ios/bottom-tabs/RNSTabBarController.h

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#import <UIKit/UIKit.h>
2+
#import "RNSTabBarAppearanceCoordinator.h"
23
#import "RNSTabsScreenViewController.h"
34

45
NS_ASSUME_NONNULL_BEGIN
@@ -23,12 +24,61 @@ NS_ASSUME_NONNULL_BEGIN
2324
@interface RNSTabBarController : UITabBarController <RNSReactTransactionObserving>
2425

2526
/**
26-
* Apply appearance to the tab bar managed by this controller.
27+
* Tab bar appearance coordinator. If you need to update tab bar appearance avoid using this one directly. Send the
28+
* controller a signal, invalidate the tab bar appearance & either wait for the update flush or flush it manually.
29+
*/
30+
@property (nonatomic, readonly, strong, nonnull) RNSTabBarAppearanceCoordinator *tabBarAppearanceCoordinator;
31+
32+
/**
33+
* Update tab controller state with previously provided children.
34+
*
35+
* This method does nothing if the children have not been changed / update has not been requested before.
36+
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
37+
* invalidation signal & let the controller decide when to flush the updates.
38+
*/
39+
- (void)updateReactChildrenControllersIfNeeded;
40+
41+
/**
42+
* Force update of the tab controller state with previously provided children.
43+
*
44+
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
45+
* invalidation signal & let the controller decide when to flush the updates.
46+
*/
47+
- (void)updateReactChildrenControllers;
48+
49+
/**
50+
* Find out which tab bar controller is currently focused & select it.
51+
*
52+
* This method does nothing if the update has not been previoulsy requested.
53+
* If needed, the requested update is performed immediately. If you do not need this, consider just raising an
54+
* appropriate invalidation signal & let the controller decide when to flush the updates.
55+
*/
56+
- (void)updateSelectedViewControllerIfNeeded;
57+
58+
/**
59+
* Find out which tab bar controller is currently focused & select it.
2760
*
28-
* @param appearance passed appearance instance will be set for all appearance modes (standard, scrollview, etc.)
29-
* supported by the tab bar. When set to `nil` the default system appearance will be used.
61+
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
62+
* invalidation signal & let the controller decide when to flush the updates.
3063
*/
31-
- (void)applyTabBarAppearance:(nullable UITabBarAppearance *)appearance;
64+
- (void)updateSelectedViewController;
65+
66+
/**
67+
* Updates the tab bar appearance basing on configuration sources (host view, tab screens).
68+
*
69+
* This method does nothing if the update has not been previoulsy requested.
70+
* If needed, the requested update is performed immediately. If you do not need this, consider just raising an
71+
* appropriate invalidation signal & let the controller decide when to flush the updates.
72+
*/
73+
- (void)updateTabBarAppearanceIfNeeded;
74+
75+
/**
76+
* Updates the tab bar appearance basing on configuration sources (host view, tab screens).
77+
*
78+
* The requested update is performed immediately. If you do not need this, consider just raising an appropriate
79+
* invalidation signal & let the controller decide when to flush the updates.
80+
*/
81+
- (void)updateTabBarAppearance;
3282

3383
@end
3484

@@ -62,6 +112,12 @@ NS_ASSUME_NONNULL_BEGIN
62112
*/
63113
@property (nonatomic, readwrite) bool needsUpdateOfSelectedTab;
64114

115+
/**
116+
* Tell the controller that some configuration regarding the tab bar apperance has changed & the appearance requires
117+
* update.
118+
*/
119+
@property (nonatomic, readwrite) bool needsUpdateOfTabBarAppearance;
120+
65121
@end
66122

67123
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)