Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 37 additions & 2 deletions packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import UIKit

private final class TabBarDelegate: NSObject, UITabBarControllerDelegate {
var onClick: ((_ index: Int) -> Bool)?
var disablePageAnimations = false

func tabBarController(
_ tabBarController: UITabBarController,
animationControllerForTransitionFrom fromVC: UIViewController,
to toVC: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
disablePageAnimations ? DisabledTabTransitionAnimator() : nil
}

func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
#if os(iOS)
Expand Down Expand Up @@ -40,7 +49,32 @@ private final class TabBarDelegate: NSObject, UITabBarControllerDelegate {
}
}

private final class DisabledTabTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
0
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to),
let toView = transitionContext.view(forKey: .to)
else {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
return
}

toView.frame = transitionContext.finalFrame(for: toViewController)

UIView.performWithoutAnimation {
transitionContext.containerView.addSubview(toView)
transitionContext.containerView.layoutIfNeeded()
}

transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}

struct TabItemEventModifier: ViewModifier {
let disablePageAnimations: Bool
let onTabEvent: (_ key: Int, _ isLongPress: Bool) -> Bool
private let delegate = TabBarDelegate()

Expand All @@ -52,6 +86,7 @@ struct TabItemEventModifier: ViewModifier {
}

func handle(tabController: UITabBarController) {
delegate.disablePageAnimations = disablePageAnimations
delegate.onClick = { index in
onTabEvent(index, false)
}
Expand Down Expand Up @@ -122,8 +157,8 @@ extension View {
/**
Event for tab items. Returns true if should prevent default (switching tabs).
*/
func onTabItemEvent(_ handler: @escaping (Int, Bool) -> Bool) -> some View {
modifier(TabItemEventModifier(onTabEvent: handler))
func onTabItemEvent(disablePageAnimations: Bool, _ handler: @escaping (Int, Bool) -> Bool) -> some View {
modifier(TabItemEventModifier(disablePageAnimations: disablePageAnimations, onTabEvent: handler))
}
}

Expand Down
23 changes: 14 additions & 9 deletions packages/react-native-bottom-tabs/ios/TabViewImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ struct TabViewImpl: View {

var body: some View {
tabContent
.disableAnimations(props.disablePageAnimations)
.tabBarMinimizeBehavior(props.minimizeBehavior)
#if !os(tvOS) && !os(macOS) && !os(visionOS)
.onTabItemEvent { index, isLongPress in
.onTabItemEvent(disablePageAnimations: props.disablePageAnimations) { index, isLongPress in
let item = props.filteredItems[safe: index]
guard let key = item?.key else { return false }

Expand Down Expand Up @@ -82,14 +83,6 @@ struct TabViewImpl: View {
.tintColor(props.selectedActiveTintColor)
.getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false)
.onChange(of: props.selectedPage ?? "") { newValue in
#if !os(macOS)
if props.disablePageAnimations {
UIView.setAnimationsEnabled(false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
UIView.setAnimationsEnabled(true)
}
}
#endif
#if os(tvOS) || os(macOS) || os(visionOS)
onSelect(newValue)
#endif
Expand Down Expand Up @@ -197,6 +190,18 @@ struct TabViewImpl: View {
#endif

extension View {
@ViewBuilder
func disableAnimations(_ disabled: Bool) -> some View {
if disabled {
self.transaction { transaction in
transaction.animation = nil
transaction.disablesAnimations = true
}
} else {
self
}
}

@ViewBuilder
func getSidebarAdaptable(enabled: Bool) -> some View {
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, *) {
Expand Down
13 changes: 12 additions & 1 deletion packages/react-native-bottom-tabs/ios/TabViewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
}

@objc public protocol TabViewProviderDelegate {
func onPageSelected(key: String, reactTag: NSNumber?)

Check warning on line 42 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
func onLongPress(key: String, reactTag: NSNumber?)

Check warning on line 43 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
func onTabBarMeasured(height: Int, reactTag: NSNumber?)

Check warning on line 44 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
func onLayout(size: CGSize, reactTag: NSNumber?)

Check warning on line 45 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
}

@objc public class TabViewProvider: PlatformView {
Expand All @@ -59,7 +59,7 @@
@objc var onTabBarMeasured: RCTDirectEventBlock?
@objc var onNativeLayout: RCTDirectEventBlock?

@objc public var icons: NSArray? {

Check warning on line 62 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
didSet {
loadIcons(icons)
}
Expand All @@ -83,9 +83,20 @@
}
}

@objc public var selectedPage: NSString? {

Check warning on line 86 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
didSet {
props.selectedPage = selectedPage as? String
let nextSelectedPage = selectedPage as? String

if disablePageAnimations {
var transaction = Transaction(animation: nil)
transaction.disablesAnimations = true

withTransaction(transaction) {
props.selectedPage = nextSelectedPage
}
} else {
props.selectedPage = nextSelectedPage
}
}
}

Expand All @@ -95,18 +106,18 @@
}
}

@objc public var layoutDirection: NSString? {

Check warning on line 109 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
didSet {
props.layoutDirection = layoutDirection as? String
}
}
@objc public var scrollEdgeAppearance: NSString? {

Check warning on line 114 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
didSet {
props.scrollEdgeAppearance = scrollEdgeAppearance as? String
}
}

@objc public var minimizeBehavior: NSString? {

Check warning on line 120 in packages/react-native-bottom-tabs/ios/TabViewProvider.swift

View workflow job for this annotation

GitHub Actions / swift-lint

Legacy Objective-C Reference Type Violation: Prefer Swift value types to bridged Objective-C reference types (legacy_objc_type)
didSet {
props.minimizeBehavior = MinimizeBehavior(rawValue: minimizeBehavior as? String ?? "")
}
Expand Down
28 changes: 24 additions & 4 deletions packages/react-native-bottom-tabs/src/TabView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ const TabView = <Route extends BaseRoute>({
tabLabelStyle,
renderBottomAccessoryView,
layoutDirection = 'locale',
disablePageAnimations = false,
...props
}: Props<Route>) => {
// @ts-ignore
Expand All @@ -274,14 +275,29 @@ const TabView = <Route extends BaseRoute>({
return navigationState.routes;
}, [navigationState.routes]);

const routeKeys = React.useMemo(
() => trimmedRoutes.map((route) => route.key),
[trimmedRoutes]
);

/**
* List of loaded tabs, tabs will be loaded when navigated to.
*/
const [loaded, setLoaded] = React.useState<string[]>([focusedKey]);

if (!loaded.includes(focusedKey)) {
const loadedRoutes = disablePageAnimations
? routeKeys
: loaded.includes(focusedKey)
? loaded
: [...loaded, focusedKey];

if (
disablePageAnimations &&
routeKeys.some((routeKey) => !loaded.includes(routeKey))
) {
setLoaded(routeKeys);
} else if (!loaded.includes(focusedKey)) {
// Set the current tab to be loaded if it was not loaded before
setLoaded((loaded) => [...loaded, focusedKey]);
setLoaded(loadedRoutes);
}

const icons = React.useMemo(
Expand Down Expand Up @@ -409,6 +425,7 @@ const TabView = <Route extends BaseRoute>({
onPageSelected={handlePageSelected}
onTabBarMeasured={handleTabBarMeasured}
onNativeLayout={handleNativeLayout}
disablePageAnimations={disablePageAnimations}
hapticFeedbackEnabled={hapticFeedbackEnabled}
layoutDirection={layoutDirection}
activeTintColor={activeTintColor}
Expand All @@ -418,7 +435,10 @@ const TabView = <Route extends BaseRoute>({
labeled={labeled}
>
{trimmedRoutes.map((route) => {
if (getLazy({ route }) !== false && !loaded.includes(route.key)) {
if (
getLazy({ route }) !== false &&
!loadedRoutes.includes(route.key)
) {
// Don't render a screen if we've never navigated to it
return (
<View
Expand Down
Loading