Skip to content

Conversation

johankasperi
Copy link
Contributor

@johankasperi johankasperi commented Oct 14, 2025

PR Description

Adds support for https://developer.apple.com/documentation/uikit/uitabbarcontroller/bottomaccessory by implementing this prop:

renderBottomAccessory={(props: { placement: "none" | "inline" | "expanded" }) => React.ReactElement}

How to test?

Use the new example in the example app named "Bottom Accessory View".

Screenshots

bottom-accessory-view.mov

@changeset-bot
Copy link

changeset-bot bot commented Oct 14, 2025

⚠️ No Changeset found

Latest commit: cb6e515

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@johankasperi
Copy link
Contributor Author

johankasperi commented Oct 17, 2025

@okwasniewski any chance you could take a look on this? Thank you!

@okwasniewski okwasniewski requested a review from Copilot October 18, 2025 10:33
Copy link

@Copilot Copilot AI left a 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.

Comment on lines +189 to +195
/**
* A function that returns a React element to display as bottom accessory view.
* iOS 26+ only.
*
* @platform ios
*/
renderBottomAccessoryView?: BottomAccessoryViewProps['renderBottomAccessoryView'];
Copy link

Copilot AI Oct 18, 2025

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)
Copy link

Copilot AI Oct 18, 2025

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).

Suggested change
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.
Copy link

Copilot AI Oct 18, 2025

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').

Suggested change
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.
Copy link

Copilot AI Oct 18, 2025

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.

Suggested change
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.

Copy link
Collaborator

@okwasniewski okwasniewski left a 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

Comment on lines +219 to +225
#### `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.
:::
Copy link
Collaborator

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

Copy link
Contributor Author

@johankasperi johankasperi Oct 20, 2025

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
Copy link
Collaborator

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

Copy link
Contributor Author

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 &&
Copy link
Collaborator

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

Copy link
Contributor Author

@johankasperi johankasperi Oct 20, 2025

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 {
Copy link
Collaborator

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

Copy link
Contributor Author

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
Copy link
Collaborator

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 114 to 115
let selectorString = "emitOnPlacementChanged:"
let selector = NSSelectorFromString(selectorString)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if (tabViewBottomAccessoryPlacement == .inline) {
if tabViewBottomAccessoryPlacement == .inline {

Copy link
Contributor Author

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) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
} else if (tabViewBottomAccessoryPlacement == .expanded) {
} else if tabViewBottomAccessoryPlacement == .expanded {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 114 to 115
let selectorString = "emitOnPlacementChanged:"
let selector = NSSelectorFromString(selectorString)
Copy link
Collaborator

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

Copy link
Contributor Author

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johankasperi
Copy link
Contributor Author

Thank you for contributing this! This is awesome 🚀

I've tested it and it works great, just few small comments

Glad you liked it and thanks for the review! Responded to some of your comments and will commit fixes to the others asap

cursor[bot]

This comment was marked as outdated.

@johankasperi
Copy link
Contributor Author

@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!

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.

2 participants