With iOS 18 Apple decided to move our beloved bottom tab bar controller to the top of the screen (apparently thats more modern). I am having the following situation: I have TabView
where we have root screen FirstView
, which has a button wrapped in NavigationStack
. When tapping on the button we push a second view where we want the tab bar hidden. However because the first screen has a title when pushing the view jumps.
Here is the code:
struct ContentView: View {
@State var path: [String] = []
@State var hideTabBar: Bool = false
var body: some View {
TabView {
navigationStack
.tabItem {
Text("Eng")
}
placeholderStack
.tabItem {
Text("Span")
}
}
}
var navigationStack: some View {
NavigationStack(path: $path) {
Text("First View")
.navigationTitle("English View")
.navigationDestination(for: String.self) {
Text($0)
.toolbar {
ToolbarItem(placement: .principal) {
Text("Second View")
}
}
}
.onTapGesture {
hideTabBar = true
path.append("Second View")
}
.onAppear {
hideTabBar = false
}
.toolbar {
ToolbarItem(placement: .principal) {
Text("Top Bar")
}
}
.toolbar(hideTabBar ? .hidden : .visible, for: .tabBar)
}
}
var placeholderStack: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hola, mundo")
}
.padding()
}
}
When navigating to the child view, it works better if the tab bar is hidden first, then navigation is performed afterwards. This can be done by setting the flag using withAnimation
and then performing the navigation in a completion
callback.
For navigating back, the transition can be made a bit smoother by resetting the flag for hiding the tab bar in .onDisappear
for the child view, with animation:
Text("First View")
.navigationTitle("English View")
.navigationDestination(for: String.self) {
Text($0)
.toolbar {
ToolbarItem(placement: .principal) {
Text("Second View")
}
}
.onDisappear {
withAnimation {
hideTabBar = false
}
}
}
.onTapGesture {
withAnimation {
hideTabBar = true
} completion: {
path.append("Second View")
}
}
.toolbar {
ToolbarItem(placement: .principal) {
Text("Top Bar")
}
}
.toolbar(hideTabBar ? .hidden : .visible, for: .tabBar)
Another way of navigating back is to replace the native back button with a custom one. This allows the flag for the tab bar to be reset first, then navigation can be performed afterwards, like before.
This approach avoids the staggered animation when navigating back. However, you lose the effect of the large navigation title morphing into the back button. It also seems to be important to set .navigationBarTitleDisplayMode(.large)
, otherwise the navigation title doesn't show when returning to the parent view.
Text("First View")
.navigationBarTitleDisplayMode(.large)
.navigationTitle("English View")
.navigationDestination(for: String.self) {
Text($0)
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .navigation) {
Button {
withAnimation {
hideTabBar = false
} completion: {
path.removeLast()
}
} label: {
HStack {
Image(systemName: "chevron.left")
.fontWeight(.semibold)
.imageScale(.large)
Text("English View")
}
}
}
ToolbarItem(placement: .principal) {
Text("Second View")
}
}
}
.onTapGesture {
withAnimation {
hideTabBar = true
} completion: {
path.append("Second View")
}
}
.toolbar {
ToolbarItem(placement: .principal) {
Text("Top Bar")
}
}
.toolbar(hideTabBar ? .hidden : .visible, for: .tabBar)