Skip to content

Commit c33164f

Browse files
Adding long Press support on android and iOS (#66)
* feat: added support for long press in android * feat: handled dynamic update tab in android * feat: ios long press event handling * fix: long press added * fix: tab long pressed event file moved * feat: ios impl added * fix: unwanted changes removed * fix: on tab long press definition changed * fix: index handling * feat: added example in native bottom tabs * feat: add event section in doc
1 parent d499559 commit c33164f

16 files changed

+244
-11
lines changed

android/src/main/java/com/rcttabview/RCTTabView.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import android.util.TypedValue
1010
import android.net.Uri
1111
import android.view.Choreographer
1212
import android.view.MenuItem
13+
import android.view.View
1314
import androidx.appcompat.content.res.AppCompatResources
1415
import com.facebook.common.references.CloseableReference
1516
import com.facebook.datasource.DataSources
@@ -29,6 +30,7 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
2930
private var isLayoutEnqueued = false
3031
var items: MutableList<TabInfo>? = null
3132
var onTabSelectedListener: ((WritableMap) -> Unit)? = null
33+
var onTabLongPressedListener: ((WritableMap) -> Unit)? = null
3234
private var isAnimating = false
3335
private var activeTintColor: Int? = null
3436
private var inactiveTintColor: Int? = null
@@ -52,6 +54,16 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
5254
}
5355
}
5456

57+
private fun onTabLongPressed(item: MenuItem) {
58+
val longPressedItem = items?.firstOrNull { it.title == item.title }
59+
longPressedItem?.let {
60+
val event = Arguments.createMap().apply {
61+
putString("key", longPressedItem.key)
62+
}
63+
onTabLongPressedListener?.invoke(event)
64+
}
65+
}
66+
5567
override fun requestLayout() {
5668
super.requestLayout()
5769
@Suppress("SENSELESS_COMPARISON") // layoutCallback can be null here since this method can be called in init
@@ -95,6 +107,12 @@ class ReactBottomNavigationView(context: Context) : BottomNavigationView(context
95107
} else {
96108
removeBadge(index)
97109
}
110+
post {
111+
findViewById<View>(menuItem.itemId).setOnLongClickListener {
112+
onTabLongPressed(menuItem)
113+
true
114+
}
115+
}
98116
}
99117
}
100118

android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ class RCTTabViewViewManager :
100100
eventDispatcher.dispatchEvent(PageSelectedEvent(viewTag = view.id, key = it))
101101
}
102102
}
103+
104+
view.onTabLongPressedListener = { data ->
105+
data.getString("key")?.let {
106+
eventDispatcher.dispatchEvent(TabLongPressEvent(viewTag = view.id, key = it))
107+
}
108+
}
109+
103110
return view
104111
}
105112

@@ -151,6 +158,8 @@ class RCTTabViewViewManager :
151158
return MapBuilder.of(
152159
PageSelectedEvent.EVENT_NAME,
153160
MapBuilder.of("registrationName", "onPageSelected"),
161+
TabLongPressEvent.EVENT_NAME,
162+
MapBuilder.of("registrationName", "onTabLongPress")
154163
)
155164
}
156165

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.rcttabview
2+
3+
import com.facebook.react.bridge.Arguments
4+
import com.facebook.react.uimanager.events.Event
5+
import com.facebook.react.uimanager.events.RCTEventEmitter
6+
7+
class TabLongPressEvent(viewTag: Int, private val key: String) :
8+
Event<TabLongPressEvent>(viewTag) {
9+
10+
companion object {
11+
const val EVENT_NAME = "onTabLongPress"
12+
}
13+
14+
override fun getEventName(): String {
15+
return EVENT_NAME
16+
}
17+
18+
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
19+
val event = Arguments.createMap().apply {
20+
putString("key", key)
21+
}
22+
rctEventEmitter.receiveEvent(viewTag, eventName, event)
23+
}
24+
}

docs/docs/docs/guides/usage-with-react-navigation.mdx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,23 @@ Badge to show on the tab icon.
149149
#### `lazy`
150150

151151
Whether this screens should render the first time it's accessed. Defaults to true. Set it to false if you want to render the screen on initial render.
152+
153+
### Events
154+
155+
The navigator can emit events on certain actions. Supported events are:
156+
157+
#### `tabLongPress`
158+
159+
This event is fired when the user presses the tab button for the current screen in the tab bar for an extended period.
160+
161+
Example:
162+
163+
```tsx
164+
React.useEffect(() => {
165+
const unsubscribe = navigation.addListener('tabLongPress', (e) => {
166+
// Do something
167+
});
168+
169+
return unsubscribe;
170+
}, [navigation]);
171+
```

example/ios/Podfile.lock

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,7 +1209,7 @@ PODS:
12091209
- ReactCommon/turbomodule/bridging
12101210
- ReactCommon/turbomodule/core
12111211
- Yoga
1212-
- react-native-bottom-tabs (0.0.10):
1212+
- react-native-bottom-tabs (0.0.12):
12131213
- DoubleConversion
12141214
- glog
12151215
- RCT-Folly (= 2024.01.01.00)
@@ -1229,6 +1229,7 @@ PODS:
12291229
- ReactCodegen
12301230
- ReactCommon/turbomodule/bridging
12311231
- ReactCommon/turbomodule/core
1232+
- SwiftUIIntrospect (~> 1.0)
12321233
- Yoga
12331234
- react-native-safe-area-context (4.11.0):
12341235
- React-Core
@@ -1545,6 +1546,7 @@ PODS:
15451546
- ReactCommon/turbomodule/core
15461547
- Yoga
15471548
- SocketRocket (0.7.0)
1549+
- SwiftUIIntrospect (1.3.0)
15481550
- Yoga (0.0.0)
15491551

15501552
DEPENDENCIES:
@@ -1621,6 +1623,7 @@ DEPENDENCIES:
16211623
SPEC REPOS:
16221624
trunk:
16231625
- SocketRocket
1626+
- SwiftUIIntrospect
16241627

16251628
EXTERNAL SOURCES:
16261629
boost:
@@ -1774,15 +1777,15 @@ SPEC CHECKSUMS:
17741777
React-CoreModules: 2d68c251bc4080028f2835fa47504e8f20669a21
17751778
React-cxxreact: bb0dc212b515d6dba6c6ddc4034584e148857db9
17761779
React-debug: fd0ed8ecd5f8a23c7daf5ceaca8aa722a4d083fd
1777-
React-defaultsnativemodule: 0d824306a15dd80e2bea12f4079fbeff9712b301
1778-
React-domnativemodule: 195491d7c1725befd636f84c67bf229203fc7d07
1780+
React-defaultsnativemodule: 371dc516e5020f8b87f1d32f8fa6872cafcc2081
1781+
React-domnativemodule: 5d1288b9b8666b818a1004b56a03befc00eb5698
17791782
React-Fabric: c12ce848f72cba42fb9e97a73a7c99abc6353f23
17801783
React-FabricComponents: 7813d5575c8ea2cda0fef9be4ff9d10987cba512
17811784
React-FabricImage: c511a5d612479cb4606edf3557c071956c8735f6
17821785
React-featureflags: cf78861db9318ae29982fa8953c92d31b276c9ac
1783-
React-featureflagsnativemodule: 54f6decea27c187c2127e3669a7f5bf2e145e637
1786+
React-featureflagsnativemodule: e774cf495486b0e2a8b324568051d6b4c722fa93
17841787
React-graphics: 7572851bca7242416b648c45d6af87d93d29281e
1785-
React-idlecallbacksnativemodule: 7d21b0e071c3e02bcc897d2c3db51319642dd466
1788+
React-idlecallbacksnativemodule: d2009bad67ef232a0ee586f53193f37823e81ef1
17861789
React-ImageManager: aedf54d34d4475c66f4c3da6b8359b95bee904e4
17871790
React-jsc: 92ac98e0e03ee54fdaa4ac3936285a4fdb166fab
17881791
React-jserrorhandler: 0c8949672a00f2a502c767350e591e3ec3d82fb3
@@ -1792,8 +1795,8 @@ SPEC CHECKSUMS:
17921795
React-jsitracing: 3935b092f85bb1e53b8cf8a00f572413648af46b
17931796
React-logger: 4072f39df335ca443932e0ccece41fbeb5ca8404
17941797
React-Mapbuffer: 714f2fae68edcabfc332b754e9fbaa8cfc68fdd4
1795-
React-microtasksnativemodule: 618b64238e43ef3154079f193aa6649e5320ae19
1796-
react-native-bottom-tabs: f2d0c291b0158846fb8c6964b9535a3ab206fdf2
1798+
React-microtasksnativemodule: 987cf7e0e0e7129250a48b807e70d3b906c726cf
1799+
react-native-bottom-tabs: 30906150a76d9735a58080792f363dc9ccee9c04
17971800
react-native-safe-area-context: 851c62c48dce80ccaa5637b6aa5991a1bc36eca9
17981801
React-nativeconfig: 4a9543185905fe41014c06776bf126083795aed9
17991802
React-NativeModulesApple: 651670a799672bd54469f2981d91493dda361ddf
@@ -1820,12 +1823,13 @@ SPEC CHECKSUMS:
18201823
React-utils: b2baee839fb869f732d617b97dcfa384b4b4fdb3
18211824
ReactCodegen: f177b8fd67788c5c6ff45a39c7482c5f8d77ace6
18221825
ReactCommon: 627bd3192ef01a351e804e9709673d3741d38fec
1823-
ReactNativeHost: 62249d6e1e42a969159946c035c1cd3f4b1035dd
1826+
ReactNativeHost: 99c0ffb175cd69de2ac9a70892cd22dac65ea79d
18241827
ReactTestApp-DevSupport: b7cd76a3aeee6167f5e14d82f09685059152c426
18251828
ReactTestApp-Resources: 7db90c026cccdf40cfa495705ad436ccc4d64154
1826-
RNGestureHandler: 366823a3ebcc5ddd25550dbfe80e89779c4760b2
1827-
RNScreens: d86f05e9c243a063ca67cda7f4e05d28fe5c31d4
1829+
RNGestureHandler: 18b9b5d65c77c4744a640f69b7fccdd47ed935c0
1830+
RNScreens: 5288a8dbeedb3c5051aa2d5658c1c553c050b80a
18281831
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
1832+
SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d
18291833
Yoga: 4ef80d96a5534f0e01b3055f17d1e19a9fc61b63
18301834

18311835
PODFILE CHECKSUM: 539add55dc6c2e7f9754e288b1ce4fd8583819ae

example/src/Examples/NativeBottomTabs.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,33 @@ import { Contacts } from '../Screens/Contacts';
44
import { Chat } from '../Screens/Chat';
55
// This import works properly when library is published
66
import createNativeBottomTabNavigator from '../../../src/react-navigation/navigators/createNativeBottomTabNavigator';
7+
import { Platform } from 'react-native';
78

89
const Tab = createNativeBottomTabNavigator();
910

1011
function NativeBottomTabs() {
1112
return (
12-
<Tab.Navigator tabBarInactiveTintColor="red" tabBarActiveTintColor="orange">
13+
<Tab.Navigator
14+
tabBarInactiveTintColor="red"
15+
tabBarActiveTintColor="orange"
16+
screenListeners={{
17+
tabLongPress: (data) => {
18+
console.log(
19+
`${Platform.OS}: Long press detected on tab with key ${data.target} at the navigator level.`
20+
);
21+
},
22+
}}
23+
>
1324
<Tab.Screen
1425
name="Article"
1526
component={Article}
27+
listeners={{
28+
tabLongPress: (data) => {
29+
console.log(
30+
`${Platform.OS}: Long press detected on tab with key ${data.target} at the screen level.`
31+
);
32+
},
33+
}}
1634
options={{
1735
tabBarBadge: '10',
1836
tabBarIcon: ({ focused }) =>

ios/RCTTabViewViewManager.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ - (UIView *)view
2424

2525
RCT_EXPORT_VIEW_PROPERTY(items, NSArray)
2626
RCT_EXPORT_VIEW_PROPERTY(onPageSelected, RCTDirectEventBlock)
27+
RCT_EXPORT_VIEW_PROPERTY(onTabLongPress, RCTDirectEventBlock)
2728
RCT_EXPORT_VIEW_PROPERTY(selectedPage, NSString)
2829
RCT_EXPORT_VIEW_PROPERTY(tabViewStyle, NSString)
2930
RCT_EXPORT_VIEW_PROPERTY(icons, NSArray<RCTImageSource *>);

ios/TabItemLongPressModifier.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import SwiftUI
2+
import SwiftUIIntrospect
3+
4+
struct TabItemLongPressModifier: ViewModifier {
5+
let onLongPress: (Int) -> Void
6+
7+
func body(content: Content) -> some View {
8+
content.introspect(.tabView, on: .iOS(.v13, .v14, .v15, .v16, .v17, .v18)) { tabController in
9+
// Remove existing long press gestures
10+
if let existingGestures = tabController.tabBar.gestureRecognizers {
11+
for gesture in existingGestures where gesture is UILongPressGestureRecognizer {
12+
tabController.tabBar.removeGestureRecognizer(gesture)
13+
}
14+
}
15+
16+
// Create gesture handler
17+
let handler = LongPressGestureHandler(tabBar: tabController.tabBar, handler: onLongPress)
18+
let gesture = UILongPressGestureRecognizer(target: handler, action: #selector(LongPressGestureHandler.handleLongPress(_:)))
19+
gesture.minimumPressDuration = 0.5
20+
21+
objc_setAssociatedObject(tabController.tabBar, &AssociatedKeys.gestureHandler, handler, .OBJC_ASSOCIATION_RETAIN)
22+
23+
tabController.tabBar.addGestureRecognizer(gesture)
24+
}
25+
}
26+
}
27+
28+
private struct AssociatedKeys {
29+
static var gestureHandler: UInt8 = 0
30+
}
31+
32+
private class LongPressGestureHandler: NSObject {
33+
private weak var tabBar: UITabBar?
34+
private let handler: (Int) -> Void
35+
36+
init(tabBar: UITabBar, handler: @escaping (Int) -> Void) {
37+
self.tabBar = tabBar
38+
self.handler = handler
39+
super.init()
40+
}
41+
42+
@objc func handleLongPress(_ recognizer: UILongPressGestureRecognizer) {
43+
guard recognizer.state == .began,
44+
let tabBar = tabBar else { return }
45+
46+
let location = recognizer.location(in: tabBar)
47+
48+
// Get buttons and sort them by frames
49+
let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).contains("UITabBarButton") }.sorted(by: { $0.frame.minX < $1.frame.minX })
50+
51+
for (index, button) in tabBarButtons.enumerated() {
52+
if button.frame.contains(location) {
53+
handler(index)
54+
break
55+
}
56+
}
57+
}
58+
}
59+
60+
extension View {
61+
func onTabItemLongPress(_ handler: @escaping (Int) -> Void) -> some View {
62+
modifier(TabItemLongPressModifier(onLongPress: handler))
63+
}
64+
}

ios/TabLongPressedEvent.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React
2+
3+
class TabLongPressEvent: NSObject, RCTEvent {
4+
var viewTag: NSNumber
5+
private var key: NSString
6+
internal var coalescingKey: UInt16
7+
8+
var eventName: String {
9+
return "onTabLongPress"
10+
}
11+
12+
init(reactTag: NSNumber, key: NSString, coalescingKey: UInt16) {
13+
self.viewTag = reactTag
14+
self.key = key
15+
self.coalescingKey = coalescingKey
16+
super.init()
17+
}
18+
19+
func canCoalesce() -> Bool {
20+
return false
21+
}
22+
23+
class func moduleDotMethod() -> String {
24+
return "RCTEventEmitter.receiveEvent"
25+
}
26+
27+
func arguments() -> [Any] {
28+
return [
29+
viewTag,
30+
RCTNormalizeInputEventName(eventName),
31+
[
32+
"key": key
33+
]
34+
]
35+
}
36+
}

ios/TabViewImpl.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ struct RepresentableView: UIViewRepresentable {
5050
struct TabViewImpl: View {
5151
@ObservedObject var props: TabViewProps
5252
var onSelect: (_ key: String) -> Void
53+
var onLongPress: (_ key: String) -> Void
5354

5455
var body: some View {
5556
TabView(selection: $props.selectedPage) {
@@ -75,6 +76,11 @@ struct TabViewImpl: View {
7576
.tabBadge(tabData?.badge)
7677
}
7778
}
79+
.onTabItemLongPress({ index in
80+
if let key = props.items?.tabs[safe: index]?.key {
81+
onLongPress(key)
82+
}
83+
})
7884
.tintColor(props.selectedActiveTintColor)
7985
.getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false)
8086
.configureAppearance(props: props)

0 commit comments

Comments
 (0)