Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/brown-clocks-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"react-native-bottom-tabs": patch
"react-native-bottom-tabs-example": patch
"react-native-bottom-tabs-docs": patch
---

feat(iOS): [experimental] implement bottom accessory view
8 changes: 4 additions & 4 deletions apps/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1748,7 +1748,7 @@ PODS:
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- SocketRocket
- react-native-bottom-tabs (0.11.2):
- react-native-bottom-tabs (0.12.0):
- boost
- DoubleConversion
- fast_float
Expand All @@ -1766,7 +1766,7 @@ PODS:
- React-graphics
- React-ImageManager
- React-jsi
- react-native-bottom-tabs/common (= 0.11.2)
- react-native-bottom-tabs/common (= 0.12.0)
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
Expand All @@ -1778,7 +1778,7 @@ PODS:
- SocketRocket
- SwiftUIIntrospect (~> 1.0)
- Yoga
- react-native-bottom-tabs/common (0.11.2):
- react-native-bottom-tabs/common (0.12.0):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2842,7 +2842,7 @@ SPEC CHECKSUMS:
React-logger: a3cb5b29c32b8e447b5a96919340e89334062b48
React-Mapbuffer: 9d2434a42701d6144ca18f0ca1c4507808ca7696
React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b
react-native-bottom-tabs: d71dd2e1b69f11d3ed2da2db23016ebdc77f4ba1
react-native-bottom-tabs: f068aaf76d89f04627dd80af56dde68efa6dd507
react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616
React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3
React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d
Expand Down
6 changes: 6 additions & 0 deletions apps/example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import NativeBottomTabsRemoteIcons from './Examples/NativeBottomTabsRemoteIcons'
import NativeBottomTabsUnmounting from './Examples/NativeBottomTabsUnmounting';
import NativeBottomTabsCustomTabBar from './Examples/NativeBottomTabsCustomTabBar';
import NativeBottomTabsFreezeOnBlur from './Examples/NativeBottomTabsFreezeOnBlur';
import BottomAccessoryView from './Examples/BottomAccessoryView';

const HiddenTab = () => {
return <FourTabs hideOneTab />;
Expand Down Expand Up @@ -150,6 +151,11 @@ const examples = [
},
{ component: MaterialBottomTabs, name: 'Material (JS) Bottom Tabs' },
{ component: TintColorsExample, name: 'Tint Colors' },
{
component: BottomAccessoryView,
name: 'Bottom Accessory View',
screenOptions: { headerShown: false },
},
];

function App() {
Expand Down
71 changes: 71 additions & 0 deletions apps/example/src/Examples/BottomAccessoryView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import TabView, { SceneMap } from 'react-native-bottom-tabs';
import { useState } from 'react';
import { Article } from '../Screens/Article';
import { Albums } from '../Screens/Albums';
import { Contacts } from '../Screens/Contacts';
import { Text, View, type TextStyle, type ViewStyle } from 'react-native';

const bottomAccessoryViewStyle: ViewStyle = {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
};

const textStyle: TextStyle = { textAlign: 'center' };

const renderScene = SceneMap({
article: Article,
albums: Albums,
contacts: Contacts,
});

export default function BottomAccessoryView() {
const [index, setIndex] = useState(0);
const [routes] = useState([
{
key: 'article',
title: 'Article',
focusedIcon: require('../../assets/icons/article_dark.png'),
badge: '!',
},
{
key: 'albums',
title: 'Albums',
focusedIcon: require('../../assets/icons/grid_dark.png'),
badge: '5',
},
{
key: 'contacts',
focusedIcon: require('../../assets/icons/person_dark.png'),
title: 'Contacts',
role: 'search',
},
]);

const [bottomAccessoryDimensions, setBottomAccessoryDimensions] = useState({
width: 0,
height: 0,
});

return (
<TabView
sidebarAdaptable
minimizeBehavior="onScrollDown"
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
renderBottomAccessoryView={({ placement }) => (
<View
style={bottomAccessoryViewStyle}
onLayout={(e) => setBottomAccessoryDimensions(e.nativeEvent.layout)}
>
<Text style={textStyle}>
Placement: {placement}. Dimensions:{' '}
{bottomAccessoryDimensions.width}x{bottomAccessoryDimensions.height}
</Text>
</View>
)}
/>
);
}
8 changes: 8 additions & 0 deletions docs/docs/docs/guides/standalone-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ Color of tab indicator.

- Type: `ColorValue`

#### `renderBottomAccessoryView` <Badge text="iOS" type="info" /> <Badge text="experimental" type="danger"/>

Function that returns a React element to render as [bottom accessory](https://developer.apple.com/documentation/uikit/uitabbarcontroller/bottomaccessory).

:::note
This feature requires iOS 26.0 or later and is only available on iOS. On older versions, this prop is ignored.
:::

### Route Configuration

Each route in the `routes` array can have the following properties:
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/docs/guides/usage-with-react-navigation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,13 @@ function MyTabs() {
);
}
```
#### `renderBottomAccessoryView` <Badge text="iOS" type="info" /> <Badge text="experimental" type="danger"/>

Function that returns a React element to render as [bottom accessory](https://developer.apple.com/documentation/uikit/uitabbarcontroller/bottomaccessory).

:::note
This feature requires iOS 26.0 or later and is only available on iOS. On older versions, this prop is ignored.
:::

### Options

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SwiftUI

@objc public class BottomAccessoryProvider: NSObject {
private weak var delegate: BottomAccessoryProviderDelegate?

@objc public convenience init(delegate: BottomAccessoryProviderDelegate) {
self.init()
self.delegate = delegate
}

@available(iOS 26.0, *)
public func emitPlacementChanged(_ placement: TabViewBottomAccessoryPlacement?) {
var placementValue = "none"
if placement == .inline {
placementValue = "inline"
} else if placement == .expanded {
placementValue = "expanded"
}
self.delegate?.onPlacementChanged(placement: placementValue)
}
}

@objc public protocol BottomAccessoryProviderDelegate {
func onPlacementChanged(placement: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#if TARGET_OS_OSX
#import <AppKit/AppKit.h>
#else
#import <UIKit/UIKit.h>
#endif

NS_ASSUME_NONNULL_BEGIN

@interface RCTBottomAccessoryComponentView: RCTViewComponentView

@end

NS_ASSUME_NONNULL_END

#endif /* RCTBottomAccessoryComponentView_h */
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#ifdef RCT_NEW_ARCH_ENABLED
#import "RCTBottomAccessoryComponentView.h"

#import <react/renderer/components/RNCTabView/ComponentDescriptors.h>
#import <react/renderer/components/RNCTabView/EventEmitters.h>
#import <react/renderer/components/RNCTabView/Props.h>
#import <react/renderer/components/RNCTabView/RCTComponentViewHelpers.h>

#import <React/RCTFabricComponentsPlugins.h>

#if __has_include("react_native_bottom_tabs/react_native_bottom_tabs-Swift.h")
#import "react_native_bottom_tabs/react_native_bottom_tabs-Swift.h"
#else
#import "react_native_bottom_tabs-Swift.h"
#endif

using namespace facebook::react;

@interface RCTBottomAccessoryComponentView () <BottomAccessoryProviderDelegate> {
BottomAccessoryProvider* bottomAccessoryProvider;
}
@end

@implementation RCTBottomAccessoryComponentView

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<BottomAccessoryViewComponentDescriptor>();
}

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const BottomAccessoryViewProps>();
if (@available(iOS 26.0, *)) {
bottomAccessoryProvider = [[BottomAccessoryProvider alloc] initWithDelegate:self];
}
}

return self;
}

- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
auto eventEmitter = std::static_pointer_cast<const BottomAccessoryViewEventEmitter>(_eventEmitter);
if (eventEmitter) {
TODO: Rewrite this to emit synchronous layout events using shadow nodes
eventEmitter->onNativeLayout(BottomAccessoryViewEventEmitter::OnNativeLayout {
.height = frame.size.height,
.width = frame.size.width
});
Comment on lines +49 to +52
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's add a TODO here, TODO: Rewrite this to emit synchronous layout events using shadow nodes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

}
}

// MARK: BottomAccessoryProviderDelegate

- (void)onPlacementChangedWithPlacement:(NSString *)placement
{
auto eventEmitter = std::static_pointer_cast<const BottomAccessoryViewEventEmitter>(_eventEmitter);
if (eventEmitter) {
eventEmitter->onPlacementChanged(BottomAccessoryViewEventEmitter::OnPlacementChanged {
.placement = std::string([placement UTF8String])
});
}
}


Class<RCTComponentViewProtocol> BottomAccessoryViewCls(void)
{
return RCTBottomAccessoryComponentView.class;
}

@end

#endif
62 changes: 62 additions & 0 deletions packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React
import SwiftUI

@available(iOS 18, macOS 15, visionOS 2, tvOS 18, *)
Expand Down Expand Up @@ -51,5 +52,66 @@ struct NewTabView: AnyTabView {
.measureView { size in
onLayout(size)
}
.modifier(ConditionalBottomAccessoryModifier(props: props))
}
}

struct ConditionalBottomAccessoryModifier: ViewModifier {
@ObservedObject var props: TabViewProps

private var bottomAccessoryView: PlatformView? {
props.children.first { child in
let className = String(describing: type(of: child.view))
return className == "RCTBottomAccessoryComponentView"
}?.view
}

func body(content: Content) -> some View {
if #available(iOS 26.0, macOS 26.0, tvOS 26.0, visionOS 3.0, *) {
content
.tabViewBottomAccessory {
renderBottomAccessoryView()
}
} else {
content
}
}

@ViewBuilder
private func renderBottomAccessoryView() -> some View {
if let bottomAccessoryView {
if #available(iOS 26.0, *) {
BottomAccessoryRepresentableView(view: bottomAccessoryView)
}
}
}
}

@available(iOS 26.0, *)
struct BottomAccessoryRepresentableView: PlatformViewRepresentable {
@Environment(\.tabViewBottomAccessoryPlacement) var tabViewBottomAccessoryPlacement
var view: PlatformView

func makeUIView(context: Context) -> PlatformView {
let wrapper = UIView()
wrapper.addSubview(view)

view.autoresizingMask = [.flexibleWidth, .flexibleHeight]

emitPlacementChanged(for: view)
return wrapper
}

func updateUIView(_ uiView: PlatformView, context: Context) {
if let subview = uiView.subviews.first {
subview.frame = uiView.bounds
}
emitPlacementChanged(for: view)
}

private func emitPlacementChanged(for uiView: PlatformView) {
if let contentView = uiView.value(forKey: "bottomAccessoryProvider") as? BottomAccessoryProvider {
contentView.emitPlacementChanged(tabViewBottomAccessoryPlacement)
}
Copy link

Choose a reason for hiding this comment

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

Bug: KVC Access Fails for Private Variable

The BottomAccessoryRepresentableView tries to access bottomAccessoryProvider via KVC, but the underlying Objective-C view's bottomAccessoryProvider is a private instance variable, not a KVC-compliant property. This prevents emitPlacementChanged from being called, so placement change events are not emitted.

Fix in Cursor Fix in Web

}
}
3 changes: 2 additions & 1 deletion packages/react-native-bottom-tabs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@
},
"ios": {
"componentProvider": {
"RNCTabView": "RCTTabViewComponentView"
"RNCTabView": "RCTTabViewComponentView",
"BottomAccessoryView": "RCTBottomAccessoryComponentView"
},
"modulesConformingToProtocol": {
"RCTImageDataDecoder": [
Expand Down
Loading
Loading