diff --git a/.changeset/eighty-crews-exist.md b/.changeset/eighty-crews-exist.md new file mode 100644 index 00000000..159ce547 --- /dev/null +++ b/.changeset/eighty-crews-exist.md @@ -0,0 +1,5 @@ +--- +'react-native-bottom-tabs': patch +--- + +fix(ios): respect `disablePageAnimations` when switching tabs on iOS. diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 3abdcd60..fee7067f 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -1246,7 +1246,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-bottom-tabs (1.4.0): + - react-native-bottom-tabs (1.5.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -1259,7 +1259,7 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-bottom-tabs/common (= 1.4.0) + - react-native-bottom-tabs/common (= 1.5.0) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -1270,7 +1270,7 @@ PODS: - ReactNativeDependencies - SwiftUIIntrospect (~> 1.0) - Yoga - - react-native-bottom-tabs/common (1.4.0): + - react-native-bottom-tabs/common (1.5.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2257,7 +2257,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 295203fc648a43acf7fba609ddf573aa9fca849f React-microtasksnativemodule: 269f81deee27a2a9bedb6b2d1088c1ae93de9bd3 React-mutationobservernativemodule: 877e9f730e0d8175c2e952901b55c170fc3d372f - react-native-bottom-tabs: 919e0cce22b40003e716a11b1c81118afffec2aa + react-native-bottom-tabs: dbef12f7c74c8af648087ea11a16b20da93a399c react-native-safe-area-context: f17f4ec987ecdd130820a4a850498b874fb70388 React-NativeModulesApple: adb1dd8361dff80e9f6652fb3fdbf033cfbe2a89 React-networking: c00b437c4408f0640ba0d4158357133488521eca diff --git a/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift b/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift index 674e602d..0581d8ee 100644 --- a/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift +++ b/packages/react-native-bottom-tabs/ios/TabItemEventModifier.swift @@ -6,8 +6,36 @@ import UIKit #if !os(macOS) && !os(visionOS) +private final class ImmediateTabTransition: NSObject, UIViewControllerAnimatedTransitioning { + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + 0 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard let destination = transitionContext.viewController(forKey: .to), + let destinationView = transitionContext.view(forKey: .to) + else { + transitionContext.completeTransition(false) + return + } + + destinationView.frame = transitionContext.finalFrame(for: destination) + transitionContext.containerView.addSubview(destinationView) + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + } +} + private final class TabBarDelegate: NSObject, UITabBarControllerDelegate { var onClick: ((_ index: Int?, _ identifier: String?) -> Bool)? + var disablePageAnimations = false + + func tabBarController( + _ tabBarController: UITabBarController, + animationControllerForTransitionFrom fromVC: UIViewController, + to toVC: UIViewController + ) -> UIViewControllerAnimatedTransitioning? { + disablePageAnimations ? ImmediateTabTransition() : nil + } func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { if #available(iOS 27.0, *) { @@ -74,6 +102,7 @@ private final class TabBarDelegate: NSObject, UITabBarControllerDelegate { } struct TabItemEventModifier: ViewModifier { + let disablePageAnimations: Bool let onTabEvent: (_ index: Int?, _ identifier: String?, _ isLongPress: Bool) -> Bool private let delegate = TabBarDelegate() @@ -85,6 +114,7 @@ struct TabItemEventModifier: ViewModifier { } func handle(tabController: UITabBarController) { + delegate.disablePageAnimations = disablePageAnimations delegate.onClick = { index, identifier in onTabEvent(index, identifier, false) } @@ -155,8 +185,11 @@ extension View { /** Event for tab items. Returns true if should prevent default (switching tabs). */ - func onTabItemEvent(_ handler: @escaping (Int?, String?, Bool) -> Bool) -> some View { - modifier(TabItemEventModifier(onTabEvent: handler)) + func onTabItemEvent( + disablePageAnimations: Bool, + _ handler: @escaping (Int?, String?, Bool) -> Bool + ) -> some View { + modifier(TabItemEventModifier(disablePageAnimations: disablePageAnimations, onTabEvent: handler)) } } diff --git a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift index 42703d59..951f8575 100644 --- a/packages/react-native-bottom-tabs/ios/TabViewImpl.swift +++ b/packages/react-native-bottom-tabs/ios/TabViewImpl.swift @@ -47,7 +47,7 @@ struct TabViewImpl: View { tabContent .tabBarMinimizeBehavior(props.minimizeBehavior) #if !os(tvOS) && !os(macOS) && !os(visionOS) - .onTabItemEvent { index, identifier, isLongPress in + .onTabItemEvent(disablePageAnimations: props.disablePageAnimations) { index, identifier, isLongPress in let item = identifier.flatMap { props.filteredItems.findByKey($0) } ?? index.flatMap { props.filteredItems[safe: $0] } guard let key = item?.key else { return false } @@ -89,7 +89,7 @@ struct TabViewImpl: View { .tintColor(props.selectedActiveTintColor) .getSidebarAdaptable(enabled: props.sidebarAdaptable ?? false) .onChange(of: props.selectedPage ?? "") { newValue in - #if !os(macOS) + #if os(tvOS) || os(visionOS) if props.disablePageAnimations { UIView.setAnimationsEnabled(false) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {