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

1,389 阅读5分钟

Demo地址:ClassicTabBarUsingDemo(主要实现代码可搜索“📌”查看)

前言

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

  • 强行给你套个玻璃罩子

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

方案一

来自大佬网友分享的方案 💪

  1. 自定义TabBar使用UITabBar,通过KVC设置(老方法):
setValue(customTabBar, forKeyPath: "tabBar")
  1. 重写UITabBaraddSubviewaddGestureRecognizer方法:
- (void)addSubview:(UIView *)view {
    if ([view isKindOfClass:NSClassFromString(@"UIKit._UITabBarPlatterView")]) {
        view.hidden = YES;
    }
    [super addSubview:view];
}

- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
    if ([gestureRecognizer isKindOfClass:NSClassFromString(@"_UIContinuousSelectionGestureRecognizer")]) {
        gestureRecognizer.enabled = NO;
    }
    [super addGestureRecognizer:gestureRecognizer];
}

解释一下:

_UITabBarPlatterView这个是显示当前Tab的玻璃罩子:

  • 把它隐藏掉就行了

_UIContinuousSelectionGestureRecognizer这个是系统用来处理TabBar切换时的动画手势,触发时会在TabBar上添加_UIPortalView这个跟随手势的玻璃罩子:

  • 同样把它禁止掉就行了

这样就相当于把UITabBar的液态玻璃“移除”掉了,是可以实现以往的显示效果👏。

只不过这个方案在pop手势滑动时,TabBar会被「置顶」显示:

  • 这是苹果新UI的显示逻辑,暂时无法改动

这跟我的预期还差了一点,我是希望连pop手势也能像以前那样:

接下来介绍另一个方案,虽然麻烦很多,但能兼顾pop手势。

方案二

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

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

  1. 首先自定义TabBar要使用UIView(如果使用的是私自改造的UITabBar,得换成UIView了),并且隐藏系统TabBar。
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容器的下面)。
class BaseViewController: UIViewController {
    /// 专门存放自定义TabBar的容器
    private let tabBarContainer = TabBarContainer()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        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容器上,这样就能完美实现切换效果了。

核心代码如下:

// 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)
        }
    }
}

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

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

小结

方案一是比较激进的魔改方案,直接把系统的玻璃罩子和手势给移除掉了,缺点是如果苹果以后改动了这些私有类名或行为,可能会导致失效。

方案二是我能想到最完美的方案了,起码不用自定义UITabBarController,简单粗暴,个人感觉能应付80%的应用场景吧,除非你有非常特殊的过场动画需要挪动TabBar的。

以上就是我的方案了,起码不用自定义UITabBarController,简单粗暴,个人感觉能应付80%的应用场景吧,除非你有非常特殊的过场动画需要挪动TabBar的。

更多细节可以参考Demo,以上两种方案都有提供,只需要在WLTabBar.h中选择使用哪一种父类并注释另一个即可:

@interface WLTabBar : UITabBar // 方案一
@interface WLTabBar : UIView // 方案二

希望苹果以后能推出兼容自定义TabBar的API,那就不用这样魔改了😩。