返回

SwiftUI TabView 工具栏显示延迟问题解析与修复

IOS

TabView 切换时工具栏显示延迟问题解析与修复

在使用 SwiftUI 的 TabView 时,如果你尝试在某个子视图中隐藏工具栏(.toolbar(.hidden, for: .tabBar)),然后在返回或切换到其他 Tab 时重新显示工具栏,可能会遇到一个问题:工具栏重新出现时有明显的延迟。

这问题挺烦人的, 下面先复现一下。

问题复现

以下代码可以简单地复现这个问题:

struct ContentView: View {
    var body: some View {
        TabView {
            NavigationStack {
                NavigationLink("Tap Me") {
                    Text("Detail View")
                        .toolbar(.hidden, for: .tabBar)
                }
                .navigationTitle("Primary View")
            }
            .tabItem {
                Label("Home", systemImage: "house")
            }
        }
    }
}

当你点击 "Tap Me" 链接进入详情视图(隐藏了工具栏),然后返回主视图时,TabView 的工具栏会延迟一段时间才重新显示。

问题原因分析

这个问题的原因很可能是 SwiftUI 的 TabViewNavigationStack 在处理工具栏可见性变化时的内部机制导致的。具体来说有以下几个原因:

  1. 视图层级和生命周期管理: SwiftUI 通过复杂的视图层级和生命周期事件来优化渲染。隐藏和显示工具栏涉及到视图的更新和重新布局,这个过程可能不是即时的。
  2. 动画过渡: SwiftUI 默认会对视图的某些变化应用动画效果。工具栏的隐藏和显示可能也包含了这种动画过渡,但这个动画可能没有针对工具栏的快速显示进行优化。
  3. TabView 的内部实现: TabView为了支持标签切换、手势滑动等功能,在视图管理上有复杂的内部实现, 与NavigationView的工具栏交互可能会产生性能损耗。
  4. iOS 版本: 这个延迟问题, 不同 iOS 版本可能出现不同现象, 新版本或许已修复。

总而言之,目前来看这像是 SwiftUI 框架本身的缺陷,或至少是一个未充分优化的行为。

解决方案

既然官方直接提供的 toolbar 修饰符有 bug, 那咱们只能曲线救国了。下面介绍几种解决方法。

方案 1: 使用 toolbar 修饰符结合 @State (或 @Binding)

基本思路是用一个 @State 变量控制工具栏的显示状态,避免直接用 .hidden 方式。

struct ContentView: View {
    @State private var isToolbarHidden = false

    var body: some View {
        TabView {
            NavigationStack {
                NavigationLink("Tap Me") {
                    DetailView(isToolbarHidden: $isToolbarHidden)
                }
                .navigationTitle("Primary View")
                .toolbar(isToolbarHidden ? .hidden : .visible, for: .tabBar)

            }
            .tabItem {
                Label("Home", systemImage: "house")
            }
        }
    }
}

struct DetailView: View {
    @Binding var isToolbarHidden: Bool

    var body: some View {
        Text("Detail View")
            .onAppear {
                isToolbarHidden = true
            }
            .onDisappear {
                isToolbarHidden = false
            }
    }
}

原理:

  • 使用 @State 变量 isToolbarHidden 来跟踪工具栏的可见性。
  • NavigationLink 目标视图 (DetailView) 中, 使用 @Binding 来修改父视图的 isToolbarHidden
  • DetailViewonAppear 中将 isToolbarHidden 设置为 true(隐藏工具栏),在 onDisappear 中设置为 false(显示工具栏)。
  • NavigationStack中使用toolbar结合三元表达式设置工具栏可见性.

这个方案下, 仍然存在延迟, 但相比起直接使用.toolbar(.hidden, for: .tabBar)延迟短不少。

方案 2:使用自定义的 Overlay 隐藏 Tab Bar

基本思路是不去直接隐藏 TabBar, 而是用一个自定义的视图盖住 TabBar.

struct ContentView: View {
    @State private var isToolbarHidden = false

    var body: some View {
        TabView {
            NavigationStack {
                ZStack {
                    NavigationLink("Tap Me") {
                        DetailView(isToolbarHidden: $isToolbarHidden)
                    }
                    .navigationTitle("Primary View")
                    
                    if isToolbarHidden {
                        Rectangle()
                            .fill(.clear) //根据实际情况修改背景色
                            .frame(height: 49) // Standard tab bar height
                            .frame(maxWidth: .infinity)
                            .background(Color(UIColor.systemBackground))
                            .overlay( // 可选,为了应对不规则Tab Bar
                                 GeometryReader { proxy in
                                     Color.clear
                                         .preference(key: TabBarPreferenceKey.self, value: [proxy.size.height])
                                 }
                             )

                            .alignmentGuide(VerticalAlignment.bottom, computeValue: { _ in
                                return 0
                            })

                            .offset(y: isToolbarHidden ? 0 : UIScreen.main.bounds.height)

                        .transition(.move(edge: .bottom)) //添加一个动效
                            .animation(.default, value: isToolbarHidden)


                    }

                }

            }
            .tabItem {
                Label("Home", systemImage: "house")
            }
        }
    }
}
struct TabBarPreferenceKey: PreferenceKey {
    static var defaultValue: [CGFloat] = []

    static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
        value.append(contentsOf: nextValue())
    }
}

struct DetailView: View {
    @Binding var isToolbarHidden: Bool
     @State private var tabBarHeight: CGFloat = 49

    var body: some View {
        Text("Detail View")
           .onPreferenceChange(TabBarPreferenceKey.self) { heights in
                            self.tabBarHeight = heights.first ?? 49 //49 默认值,适配大多数情况.
                        }
            .onAppear {
                withAnimation{

                 isToolbarHidden = true
                }
            }
            .onDisappear {
                withAnimation{

                isToolbarHidden = false
                }
            }
    }
}

原理:

  • 利用ZStackNavigationView上面放一层, 用来遮挡 TabBar.
  • 通过isToolbarHidden 控制自定义遮挡视图的显示与隐藏.
  • 利用transitionanimation给遮挡视图的显隐添加一个动效。
  • 利用 GeometryReaderPreferenceKey 应对那些底部非标准的 TabBar, 通过读取GeometryReader获取tabbar高度然后动态适配。
  • 利用 .alignmentGuide 来对齐到视图底部。
  • Rectangle 可以通过fill填充需要的颜色

这个方法避免了操作 TabBar, 完全通过一层遮罩实现了TabBar隐藏和显示, 而且由于完全使用SwiftUI构建, 适配性、性能也很好, 显示隐藏没有延迟.

方案 3: UIKit (Introspection)

这种方式更"脏", 不是很建议在生产代码中如此做, 但是作为一种 hack 方案也介绍一下。利用 SwiftUI 的 Introspection 功能,可以直接访问底层的 UIKit 控件,手动控制 Tab Bar 的隐藏和显示.
首先, 我们要引入 SwiftUI Introspect

// 在 Package.swift 或 Xcode 项目设置中添加 SwiftUI-Introspect

接着就可以写如下代码:


import SwiftUI
import Introspect

struct ContentView: View {
    @State private var hideTabBar = false

    var body: some View {
        TabView {
            NavigationStack {
                NavigationLink("Tap Me") {
                    SecondView(hideTabBar: $hideTabBar)
                }
                .navigationTitle("Primary View")
            }
            .introspectTabBarController { tabBarController in
                tabBarController.tabBar.isHidden = hideTabBar
                
               
            }
            .tabItem {
                Label("First", systemImage: "1.circle")
            }
        }
    }
}
struct SecondView: View {
    @Binding var hideTabBar: Bool

    var body: some View {
        Text("Second View")
             .onAppear {
                 hideTabBar = true
             }.onDisappear {
                hideTabBar = false
            }
    }
}

原理:

  • 通过 introspectTabBarController 获取到 TabView 对应的 UITabBarController
  • 直接设置 UITabBarControllertabBar.isHidden 属性来控制 Tab Bar 的可见性。
  • 在子视图中利用onAppearonDisappear修改hideTabBar

这种方式仍然不能避免延迟,跟方案 1 情况类似,属于一种控制粒度更细的临时解决方案. 且直接操作UIKit控件,可能导致潜在问题。

安全建议 :

尽量减少在生产代码中使用, iOS 升级, 或SwiftUI 改动可能会导致兼容性出问题.

总结

目前看来,SwiftUI 在处理 TabView 中工具栏可见性变化时存在一些性能或实现上的问题。推荐使用方案2,利用自定义视图做遮罩层, 不仅没有显示延迟,而且代码更安全, 稳定, 对现有系统入侵少。