【iOS】如何在 iOS 26 的UITabBarController中使用自定义TabBar

Demo地址:ClassicTabBarUsingDemo(主要实现代码可在Demo中搜索"📌"查看)

苹果自 iOS 26 起就使用全新的UI --- Liquid Glass,导致很多系统组件也被迫强制使用,首当其冲就是UITabBarController,对于很多喜欢使用自定义TabBar的开发者来说,这很是无奈:

  • 强行给你套个玻璃罩子

如何在 iOS 26UITabBarController继续使用自定义TabBar呢?这里介绍一下我的方案。

实现方案

经观察,以往TabBar的显示效果,个人猜测系统是把TabBar放到当前子VC的view上:

按照这个思路可以这么实现:

  1. 首先自定义TabBar 要使用UIView(如果使用的是私自改造的UITabBar,得换成UIView了),并且隐藏系统TabBar。
swift 复制代码
class MainTabBarController: UITabBarController {
    ......
    
    /// 自定义TabBar
    private let customTabBar = WLTabBar()
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // 隐藏系统TabBar
        setTabBarHidden(true, animated: false)
    }
    
    ......
}
  1. TabBarController及其子VC都创建一个专门存放自定义TabBar的容器,且层级必须是最顶层 (之后添加的子视图都得插到TabBar容器的下面)。
swift 复制代码
class BaseViewController: UIViewController {
    /// 专门存放自定义TabBar的容器
    private let tabBarContainer = TabBarContainer()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        guard let tabBarContainer else { return }
        tabBarContainer.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tabBarContainer)
        NSLayoutConstraint.activate([
            tabBarContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tabBarContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tabBarContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            tabBarContainer.heightAnchor.constraint(equalToConstant: Env.tabBarFullH) // 下巴+TabBar高度
        ])
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // 层级必须是最顶层
        view.bringSubviewToFront(tabBarContainer)
    }
    
    // 将自定义TabBar放到自己的TabBar容器上
    func addTabBar(_ tabBar: UIView) {
        tabBar.superview?.isUserInteractionEnabled = false
        tabBarContainer.addSubview(tabBar)
        tabBarContainer.isUserInteractionEnabled = true
    }
}
  1. 最后,TabBarController当前显示哪个子VC,就把自定义TabBar 放到对应子VC的TabBar容器 上,这样则不会影响pushpresent其他VC。

OK,完事了。

注意点

核心实现就是以上3点,接下来讲一下注意点。

上面说到,TabBarController也得创建一个TabBar容器,这主要是用来切换子VC的:

在切换子VC前,自定义TabBar必须先放到TabBarController的TabBar容器上,切换后再放到目标子VC的TabBar容器上。

为什么?

一般子VC的内容都是懒加载(看到才构建),如果是很复杂的界面,不免会有卡顿的情况,如果直接把自定义TabBar丢过去,TabBar会闪烁一下,效果不太好;另外自 iOS 18 起切换子VC会带有默认的系统动画,其动画作用于子VC的view上,即便该子VC早就构建好,立马转移TabBar也会闪烁一下。

因此个人建议先把自定义TabBarTabBarControllerTabBar容器 上(层级在所有子VC的view之上),延时一下(确保子VC完全构建好且已完全显示,同时避免被系统动画影响)再放到目标子VC的TabBar容器上,这样就能完美实现切换效果了。

核心代码如下:

swift 复制代码
// MARK: - 挪动TabBar到目标子VC
private extension MainTabBarController {
    func moveTabBar(from sourceIdx: Int, to targetIdx: Int) {
        guard Env.isUsingLiquidGlassUI else { return }
        
        // #1 取消上一次的延时操作
        moveTabBarWorkItem?.cancel()
        moveTabBarWorkItem = nil
        
        guard let viewControllers, viewControllers.count > 0 else {
            addTabBar(customTabBar)
            return
        }
        
        guard sourceIdx != targetIdx else {
            _moveTabBar(to: targetIdx)
            return
        }
        
        // #2 如果「当前子VC」现在不是处于栈顶,就把tabBar直接挪到「目标子VC」
        let sourceNavCtr = viewControllers[sourceIdx] as? UINavigationController
        if (sourceNavCtr?.viewControllers.count ?? 0) > 1 {
            _moveTabBar(to: targetIdx)
            return
        }
        
        // #3 能来这里说明「当前子VC」正处于栈顶,如果「目标子VC」此时也处于栈顶,就把tabBar放到层级顶部(不受系统切换动画的影响)
        let targetNavCtr = viewControllers[targetIdx] as? UINavigationController
        if (targetNavCtr?.viewControllers.count ?? 0) == 1 {
            addTabBar(customTabBar)
        } else {
            _moveTabBar(to: sourceIdx)
        }
        
        // #3.1 延迟0.5s后再放入到「目标子VC」,给VC有足够时间去初始化和显示(可完美实现旧UI的效果;中途切换会取消这个延时操作#1)
        moveTabBarWorkItem = Asyncs.mainDelay(0.5) { [weak self] in
            guard let self, self.selectedIndex == targetIdx else { return }
            self.moveTabBarWorkItem = nil
            self._moveTabBar(to: targetIdx)
        }
    }
    
    func _moveTabBar(to index: Int) {
        let tab = MainTab(index: index)
        switch tab {
        case .videoHub:
            videoHubVC.addTabBar(customTabBar)
        case .channel:
            channelVC.addTabBar(customTabBar)
        case .live:
            liveVC.addTabBar(customTabBar)
        case .mine:
            mineVC.addTabBar(customTabBar)
        }
    }
}

如果想移除系统切换动画可以这么做:

swift 复制代码
// MARK: - <WLTabBarDelegate>
extension MainTabBarController: WLTabBarDelegate {
    func tabBar(_ tabBar: WLTabBar!, didSelectItemAt index: Int) {
        moveTabBar(from: selectedIndex, to: index)
        // 想移除系统自带的切换动画就👇🏻
        UIView.performWithoutAnimation {
            self.selectedIndex = index
        }
    }
}

以上就是我的方案了,起码不用自定义UITabBarController,更加简单粗暴,更多细节可以参考Demo

个人感觉能应付80%的应用场景吧,除非你有非常特殊的过场动画需要挪动TabBar的。当然希望苹果以后能推出兼容自定义TabBar的API,那就不用这样魔改了。

相关推荐
pop_xiaoli7 小时前
OC-实现下载单例类
ios·objective-c·cocoa·xcode
zhyongrui8 小时前
SnipTrip 菜单 Liquid Glass 实现方案:结构、材质、交互与深浅色策略
ios·性能优化·swiftui·交互·开源软件·材质
zhyongrui9 小时前
SnipTrip 不发烫的实现路径:局部刷新 + 合成缓存 + 峰值削减
ios·swiftui
晚霞的不甘10 小时前
Flutter for OpenHarmony 实现 iOS 风格科学计算器:从 UI 到表达式求值的完整解析
前端·flutter·ui·ios·前端框架·交互
初级代码游戏1 天前
iOS开发 SwiftUI 14:ScrollView 滚动视图
ios·swiftui·swift
初级代码游戏1 天前
iOS开发 SwitftUI 13:提示、弹窗、上下文菜单
ios·swiftui·swift·弹窗·消息框
zhyongrui1 天前
托盘删除手势与引导体验修复:滚动冲突、画布消失动画、气泡边框
ios·性能优化·swiftui·swift
zhangfeng11331 天前
CSDN星图 支持大模型微调 trl axolotl Unsloth 趋动云 LLaMA-Factory Unsloth ms-swift 模型训练
服务器·人工智能·swift
Boxsc_midnight1 天前
【openclaw+imessage】【免费无限流量】集成方案,支持iphone手机+macos
ios·智能手机·iphone
感谢地心引力2 天前
安卓、苹果手机无线投屏到Windows
android·windows·ios·智能手机·安卓·苹果·投屏