Files
DIYgodandClaude Opus 5.5 181327be31 fix(mobile): keep the native tab bar visible with the iOS 27 SDK
When the app is linked against the iOS 27 SDK, UITabBarController hosts its
tab bar in a new container where setting isTabBarHidden to false no longer
clears tabBar.isHidden. CustomTabbarController hid the bar unconditionally
and only unhid it through isTabBarHidden, so the Liquid Glass tab bar never
appeared on iOS 27. Builds linked against the iOS 26 SDK still use the old
container and were not affected.

Only hide the native bar where React Native renders its own tab bar
(iPad and systems before iOS 26).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:54:36 +08:00

178 lines
5.2 KiB
Swift

//
// TabBarRootView.swift
// FollowNative
//
// Created by Innei on 2025/3/16.
//
import ExpoModulesCore
import Foundation
import SnapKit
import UIKit
@MainActor
enum CustomTabbarController {
static var tabBarController = {
let tabBarController = UITabBarController()
let isPad = UIDevice.current.userInterfaceIdiom == .pad
if #available(iOS 16.0, *), UIDevice.current.userInterfaceIdiom == .pad {
tabBarController.tabBar.isTranslucent = false
tabBarController.tabBar.barStyle = .default
}
if #available(iOS 26.0, *), !isPad {
// The native Liquid Glass tab bar is shown, so never hide it. Apps linked against the
// iOS 27 SDK no longer have `isTabBarHidden = false` undo `tabBar.isHidden = true`.
tabBarController.tabBarMinimizeBehavior = .onScrollDown
} else {
// React Native renders the tab bar (BottomTabs) on older systems and on iPad.
tabBarController.tabBar.isHidden = true
if #available(iOS 18.0, *) {
tabBarController.isTabBarHidden = true
}
}
tabBarController.tabBar.tintColor = Utils.accentColor
return tabBarController
}()
}
class TabBarRootView: ExpoView {
private var tabBarController = CustomTabbarController.tabBarController
private let vc = UIViewController()
private var tabViewControllers: [UIViewController] = []
private var bottomAccessoryView: UIView?
private let onTabIndexChange = EventDispatcher()
private let onTabItemPress = EventDispatcher()
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
tabBarController.delegate = self
addSubview(vc.view)
vc.addChild(tabBarController)
vc.view!.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
vc.view.addSubview(tabBarController.view)
tabBarController.didMove(toParent: vc)
}
#if RCT_NEW_ARCH_ENABLED
override func mountChildComponentView(_ childComponentView: UIView, index: Int) {
insertSubview(childComponentView, at: index)
}
override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
willRemoveSubview(childComponentView)
// gestureRecognizers?.removeAll()
}
#endif
public func switchToTab(index: Int) {
let beforeIndex = tabBarController.selectedIndex
if beforeIndex == index {
return
}
if tabViewControllers.count < beforeIndex || tabViewControllers.count < index {
return
}
if let fromView = tabViewControllers[beforeIndex].view,
let toView = tabViewControllers[index].view
{
if fromView != toView {
UIView.transition(
from: fromView,
to: toView,
duration: 0.1,
options: [.transitionCrossDissolve, .preferredFramesPerSecond60],
completion: nil
)
}
}
tabBarController.selectedIndex = index
if let selectedViewController = tabBarController.selectedViewController {
tabBarController(tabBarController, didSelect: selectedViewController)
}
}
override func insertSubview(_ subview: UIView, at atIndex: Int) {
if let tabScreenView = subview as? TabScreenView {
let screenVC = UIViewController()
screenVC.view = tabScreenView
tabScreenView.ownerViewController = screenVC
// Apply current title if already provided from React side
if let currentTitle = tabScreenView.title {
screenVC.tabBarItem.title = currentTitle
}
if let icon = tabScreenView.icon {
screenVC.tabBarItem.image = .init(UIImage(named: icon)!)
}
if let activeIcon = tabScreenView.activeIcon {
screenVC.tabBarItem.selectedImage = .init(UIImage(named: activeIcon)!)
}
tabViewControllers.append(screenVC)
tabBarController.viewControllers = tabViewControllers
tabBarController.didMove(toParent: vc)
return
}
if let tabBarPortalView = subview as? TabBarPortalView {
let tabBarView = tabBarController.view!
tabBarView.addSubview(tabBarPortalView)
return
}
super.insertSubview(subview, at: atIndex)
}
override func willRemoveSubview(_ subview: UIView) {
if let tabScreenView = subview as? TabScreenView {
tabViewControllers.removeAll { viewController in
viewController.view == tabScreenView
}
tabBarController.viewControllers = tabViewControllers
tabBarController.didMove(toParent: vc)
}
}
}
// MARK: - UITabBarControllerDelegate
extension TabBarRootView: UITabBarControllerDelegate {
func tabBarController(
_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController
) -> Bool {
if let index = tabViewControllers.firstIndex(of: viewController) {
onTabItemPress(["index": index, "currentIndex": tabBarController.selectedIndex])
}
return true
}
func tabBarController(
_ tabBarController: UITabBarController, didSelect viewController: UIViewController
) {
onTabIndexChange([
"index": tabBarController.selectedIndex
])
}
// func tabBarController(
// _ tabBarController: UITabBarController,
// animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController
// ) -> (any UIViewControllerAnimatedTransitioning)? {
// return nil
// }
}