-
Couldn't load subscription status.
- 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
Changes from all commits
98bdead
4fecda5
22d4208
e56a051
70a51df
9478d1b
20ade50
cde474b
787aaeb
abb4666
7cbed96
9d44ef6
078939b
28af2bc
e6d79df
764153c
cb6e515
8dfd717
6107658
11aabc6
40b59bc
0e9d7a3
d02504c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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> | ||
| )} | ||
| /> | ||
| ); | ||
| } |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a TODO here, There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import React | ||
okwasniewski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| import SwiftUI | ||
|
|
||
| @available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) | ||
|
|
@@ -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) | ||
okwasniewski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| private func emitPlacementChanged(for uiView: PlatformView) { | ||
| if let contentView = uiView.value(forKey: "bottomAccessoryProvider") as? BottomAccessoryProvider { | ||
| contentView.emitPlacementChanged(tabViewBottomAccessoryPlacement) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: KVC Access Fails for Private VariableThe |
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.