In my SwiftUI app I have a basic tab view. For one of the detail views I want to hide the tab bar, displaying it again when a user navigates back. By default the tab bar disappears and reappears gracelessly, popping into and out of existence. I have got the disappearing of the tab view to animate, but reappearing isn't animated and the method I'm using is deprecated. Is there a way to animate showing and hiding the tab bar?
struct ContentView: View {
@State private var selectedTab: AppTab
var body: some View {
TabView(selection: $selectedTab) {
TabOne()
.tabItem {
Label(AppTab.tabone.title, systemImage: AppTab.tabone.iconName)
}
.tab(AppTab.tabone)
TabTwo()
.tabItem {
Label(AppTab.tabtwo.title, systemImage: AppTab.tabtwo.iconName)
}
.tab(AppTab.tabtwo)
TabThree()
.tabItem {
Label(AppTab.tabthree.title, systemImage: AppTab.tabthree.iconName)
}
.tab(AppTab.tabthree)
}
}
}
struct TabOne: View {
var body: some View {
NavigationStack {
NavigationLink(destination: {
Text("Details View")
.toolbar(.hidden, for: .tabBar)
// Adding animation causes the tab bar to animate when disappearing, but doesn't animate when the user navigates back and the tab bar is shown again.
// animation is also deprecated
.animation(.easeInOut(duration: 0.5))
}, label: {
Label("Details", systemImage: "person.crop.circle")
}
}
}
}
I have tried to use withAnimation
, but that didn't work.
.toolbar(withAnimation(.easeInOut(duration: 0.5)) { .hidden }, for: .tabBar)
In my SwiftUI app I have a basic tab view. For one of the detail views I want to hide the tab bar, displaying it again when a user navigates back. By default the tab bar disappears and reappears gracelessly, popping into and out of existence. I have got the disappearing of the tab view to animate, but reappearing isn't animated and the method I'm using is deprecated. Is there a way to animate showing and hiding the tab bar?
struct ContentView: View {
@State private var selectedTab: AppTab
var body: some View {
TabView(selection: $selectedTab) {
TabOne()
.tabItem {
Label(AppTab.tabone.title, systemImage: AppTab.tabone.iconName)
}
.tab(AppTab.tabone)
TabTwo()
.tabItem {
Label(AppTab.tabtwo.title, systemImage: AppTab.tabtwo.iconName)
}
.tab(AppTab.tabtwo)
TabThree()
.tabItem {
Label(AppTab.tabthree.title, systemImage: AppTab.tabthree.iconName)
}
.tab(AppTab.tabthree)
}
}
}
struct TabOne: View {
var body: some View {
NavigationStack {
NavigationLink(destination: {
Text("Details View")
.toolbar(.hidden, for: .tabBar)
// Adding animation causes the tab bar to animate when disappearing, but doesn't animate when the user navigates back and the tab bar is shown again.
// animation is also deprecated
.animation(.easeInOut(duration: 0.5))
}, label: {
Label("Details", systemImage: "person.crop.circle")
}
}
}
}
I have tried to use withAnimation
, but that didn't work.
.toolbar(withAnimation(.easeInOut(duration: 0.5)) { .hidden }, for: .tabBar)
Share
Improve this question
edited Mar 10 at 15:01
Benzy Neez
23.7k3 gold badges15 silver badges45 bronze badges
asked Mar 10 at 11:43
PieterPieter
2171 gold badge4 silver badges16 bronze badges
2 Answers
Reset to default 1When the tab bar is hidden, two changes happen:
- First, the tab bar itself is removed.
- After the tab bar has been removed, the space it was using is made available to the page content.
It seems that these changes happen in sequence, not simultaneously. This probably means, the animations can only happen in sequence too:
To animate the removal of the tab bar, use a state variable. This is described in the answers to Animating
TabBar
visibility transitions in SwiftUI (and repeated in another answer here).I found it works well to change the state value
withAnimation
in separate.onAppear
callbacks for the navigation link and the detail content.To animate the change in content height, the content can be nested in a
GeometryReader
. This is used to measure the height available, which is then set asmaxHeight
on the nested content.I found that it works best to use a separate
GeometryReader
for the detail view.
Here is how the changes can be applied to the example:
struct TabOne: View {
@State var toolbarVisibility = Visibility.visible
var body: some View {
GeometryReader { outer in
NavigationStack {
NavigationLink {
GeometryReader { inner in
Text("Details View")
.frame(maxWidth: .infinity, maxHeight: inner.size.height)
.animation(.default, value: inner.size.height)
}
.onAppear {
withAnimation { toolbarVisibility = .hidden }
}
} label: {
Label("Details", systemImage: "person.crop.circle")
}
.onAppear {
withAnimation { toolbarVisibility = .visible }
}
}
.toolbar(toolbarVisibility, for: .tabBar)
.frame(maxWidth: .infinity, maxHeight: outer.size.height)
.animation(.default, value: outer.size.height)
}
}
}
If you want to avoid the staggered animation, then another way to solve would be to use a custom tab bar. An example implementation can be found in this answer (it was my answer).
The change to a custom tab bar is quite easy in your case, because you are already using an enum for the tab options.
Here is the fully updated example to show it working. The background to the tab bar is commented out, so that it looks the same as the previous example. You might want to implement some logic that decides when to show or hide the background.
struct TabLabelStyle: LabelStyle {
let isSelected: Bool
func makeBody(configuration: Configuration) -> some View {
VStack(spacing: 4) {
configuration.icon
.imageScale(.large)
configuration.title
.font(.caption)
}
.symbolVariant(isSelected ? .fill : .none)
.foregroundStyle(isSelected ? Color.accentColor : .secondary)
.frame(maxWidth: .infinity)
}
}
struct CustomTabBar: View {
@Binding var selection: AppTab
var body: some View {
HStack {
ForEach(AppTab.allCases, id: \.self) { type in
Button {
withAnimation(.spring) {
selection = type
}
} label: {
Label(type.title, systemImage: type.iconName)
.labelStyle(TabLabelStyle(isSelected: selection == type))
}
}
}
.padding()
// .background(.bar)
// .overlay(alignment: .top) { Divider() }
}
}
struct ContentView: View {
@State private var selectedTab = AppTab.tabone
@State private var toolbarVisibility = Visibility.visible
var body: some View {
TabView(selection: $selectedTab) {
TabOne(toolbarVisibility: $toolbarVisibility)
.tag(AppTab.tabone)
.toolbarVisibility(.hidden, for: .tabBar)
TabTwo()
.tag(AppTab.tabtwo)
.toolbarVisibility(.hidden, for: .tabBar)
TabThree()
.tag(AppTab.tabthree)
.toolbarVisibility(.hidden, for: .tabBar)
}
.safeAreaInset(edge: .bottom) {
if toolbarVisibility == .visible {
CustomTabBar(selection: $selectedTab)
.transition(.asymmetric(
insertion: .push(from: .bottom),
removal: .push(from: .top)
))
}
}
}
}
struct TabOne: View {
@Binding var toolbarVisibility: Visibility
var body: some View {
NavigationStack {
NavigationLink {
Text("Details View")
.onAppear {
withAnimation { toolbarVisibility = .hidden }
}
} label: {
Label("Details", systemImage: "person.crop.circle")
}
.onAppear {
withAnimation { toolbarVisibility = .visible }
}
}
}
}
Depend the visibility of the toolbar to a value and change it with animation when needed:
@State var isTabBarVisible = true
.toolbar(isTabBarVisible ? .visible : .hidden, for: .tabBar)
withAnimation {
isTabBarVisible.toggle()
}