-
Notifications
You must be signed in to change notification settings - Fork 70
feat(iOS): implement bottom accessory view #446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat(iOS): implement bottom accessory view #446
Conversation
|
@okwasniewski any chance you could take a look on this? Thank you! |
There was a problem hiding this 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 iOS bottom accessory view support by exposing a renderBottomAccessoryView prop and wiring it to a new native component and SwiftUI integration.
- Introduces renderBottomAccessoryView prop to render a bottom accessory view on iOS.
- Adds a new native component (BottomAccessoryView) and iOS component view (RCTBottomAccessoryComponentView) with SwiftUI modifier to attach as TabView bottom accessory.
- Updates docs and example app to demonstrate usage.
Reviewed Changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
File | Description |
---|---|
packages/react-native-bottom-tabs/src/TabView.tsx | Exposes renderBottomAccessoryView prop and conditionally renders BottomAccessoryView on iOS. |
packages/react-native-bottom-tabs/src/BottomAccessoryViewNativeComponent.ts | Declares the native component interface via codegen. |
packages/react-native-bottom-tabs/src/BottomAccessoryView.tsx | Wraps the native component, tracks layout and placement, and renders user content. |
packages/react-native-bottom-tabs/package.json | Registers BottomAccessoryView iOS component provider. |
packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift | Integrates SwiftUI tabViewBottomAccessory and emits placement changes. |
packages/react-native-bottom-tabs/ios/RCTBottomAccessoryComponentView.mm | Implements the Fabric view and event emission for layout and placement. |
packages/react-native-bottom-tabs/ios/RCTBottomAccessoryComponentView.h | Declares the Fabric view interface. |
docs/docs/docs/guides/usage-with-react-navigation.mdx | Documents the new renderBottomAccessoryView prop. |
docs/docs/docs/guides/standalone-usage.md | Documents the new renderBottomAccessoryView prop. |
apps/example/src/Examples/BottomAccessoryView.tsx | Example demonstrating bottom accessory usage. |
apps/example/src/App.tsx | Adds the new example to the example app menu. |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
/** | ||
* A function that returns a React element to display as bottom accessory view. | ||
* iOS 26+ only. | ||
* | ||
* @platform ios | ||
*/ | ||
renderBottomAccessoryView?: BottomAccessoryViewProps['renderBottomAccessoryView']; |
Copilot
AI
Oct 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring states 'iOS 26+ only', which doesn't exist. Update to the actual minimum supported version (e.g., 'iOS 18+ only') to match platform availability.
Copilot uses AI. Check for mistakes.
} | ||
|
||
func updateUIView(_ uiView: PlatformView, context: Context) { | ||
emitPlacementChanged(for: view) |
Copilot
AI
Oct 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The uiView parameter is unused; calling emitPlacementChanged on the property view is confusing. Use the parameter to reflect the updated instance and avoid dead parameters: emitPlacementChanged(for: uiView).
emitPlacementChanged(for: view) | |
emitPlacementChanged(for: uiView) |
Copilot uses AI. Check for mistakes.
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. |
Copilot
AI
Oct 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update the stated minimum iOS version; 'iOS 26.0' does not exist. Replace with the correct version that introduces the bottom accessory (e.g., 'iOS 18.0 or later').
This feature requires iOS 26.0 or later and is only available on iOS. On older versions, this prop is ignored. | |
This feature requires iOS 18.0 or later and is only available on iOS. On older versions, this prop is ignored. |
Copilot uses AI. Check for mistakes.
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. |
Copilot
AI
Oct 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct the minimum iOS version reference; 'iOS 26.0' is invalid. Use the actual minimum supported version (e.g., 'iOS 18.0 or later') to align with platform availability and code.
This feature requires iOS 26.0 or later and is only available on iOS. On older versions, this prop is ignored. | |
This feature requires iOS 15.0 or later and is only available on iOS. On older versions, this prop is ignored. |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for contributing this! This is awesome 🚀
I've tested it and it works great, just few small comments
#### `renderBottomAccessoryView` <Badge text="iOS" type="info" /> | ||
|
||
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. | ||
::: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for react navigation integration it might make more sense to put it into options, so users can render different accessory view per screen. Should be fairly simple change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree with this. I think the most common use case (and the way Apple have used bottom accessory in most of their apps) is displaying a music/audio player in the bottom accessory view. In this case you want to have the same component and state across all tabs. I also think this is why Apple has designed the API so the bottom accessory is a property on UITabBar
and not on UITabBarItem
.
Also, if the user want to render different accessories on different tabs they could use useRoute
or something to identify the current tab and switch component accordingly. And having the integration on screen options will make it more difficult to have the same component across all screens and restore local state etc.
What do you think?
@@ -1,4 +1,5 @@ | |||
import SwiftUI | |||
import React |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you run swiftlint --fix ./packages/
in the root? To fix this lint issues
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
); | ||
})} | ||
{Platform.OS === 'ios' && | ||
parseFloat(Platform.Version) >= 26 && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this version check? I guess it might not be enough anyways as an app can run on iOS 26 compiled with old Xcode or with a flag that's opting out of new design.
We should probably render it anyways and handle it on the native side
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it would be better not to render the view on the JS side at all on <iOS26 and Android because it wont be visible at all on those platforms?
|
||
@ViewBuilder | ||
private func renderBottomAccessoryView() -> some View { | ||
if let accessoryView = bottomAccessoryView { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit you can use shorthand to unwrap: if let bottomAccessoryView
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
func makeUIView(context: Context) -> PlatformView { | ||
emitPlacementChanged(for: view) | ||
return view |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sure to wrap the React Native view with additional UIView, same as described here: https://github.com/callstackincubator/react-native-bottom-tabs/blob/main/packages/react-native-bottom-tabs/ios/RepresentableView.swift
It makes sure that the view we are retaining doesn't have weird artifacts when rendering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used RepresentableView
at first but when doing that SwiftUI wasn't sizing the react view properly because of RepresentableView created a wrapper view and adding the react view as a child to that. But I can give it another go
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let selectorString = "emitOnPlacementChanged:" | ||
let selector = NSSelectorFromString(selectorString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let selectorString = "emitOnPlacementChanged:" | |
let selector = NSSelectorFromString(selectorString) | |
let selector = NSSelectorFromString("emitOnPlacementChanged:") |
let selector = NSSelectorFromString(selectorString) | ||
if uiView.responds(to: selector) { | ||
var placementValue = "none" | ||
if (tabViewBottomAccessoryPlacement == .inline) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (tabViewBottomAccessoryPlacement == .inline) { | |
if tabViewBottomAccessoryPlacement == .inline { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var placementValue = "none" | ||
if (tabViewBottomAccessoryPlacement == .inline) { | ||
placementValue = "inline" | ||
} else if (tabViewBottomAccessoryPlacement == .expanded) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} else if (tabViewBottomAccessoryPlacement == .expanded) { | |
} else if tabViewBottomAccessoryPlacement == .expanded { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let selectorString = "emitOnPlacementChanged:" | ||
let selector = NSSelectorFromString(selectorString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could be solved by a shared protocol between RCTBottomAccessoryView
and NewTabView
this way we would have type safety
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure! I will do that asap
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Glad you liked it and thanks for the review! Responded to some of your comments and will commit fixes to the others asap |
@okwasniewski I think I've made all the changes you requested or responded to your comments. Would be awesome if you could have a look and review it again. Thank you! |
PR Description
Adds support for https://developer.apple.com/documentation/uikit/uitabbarcontroller/bottomaccessory by implementing this prop:
How to test?
Use the new example in the example app named "Bottom Accessory View".
Screenshots
bottom-accessory-view.mov