I'm trying to design a SwiftUI view with a tab bar positioned at the top of the screen, including buttons on the leading and trailing sides of the tab bar, similar to the layout of the tab bar in Apple's Files app on iPad (refer to the screenshot)
I currently have the following implementation using TabView:
struct MainView: View {
var body: some View {
NavigationStack {
// Main TabView placed on top of the app bar, below the toolbar
TabView {
HomeView2()
.tabItem {
Label("Home", systemImage: "house")
}
BrowseView()
.tabItem {
Label("Browse", systemImage: "magnifyingglass")
}
ProfileView2()
.tabItem {
Label("Profile", systemImage: "person")
}
}
.toolbar(content: {
ToolbarItem(placement: .topBarTrailing) {
Image(systemName: "heart.fill")
}
})
.navigationBarItems(
leading: Image(systemName: "heart.fill"),
trailing: Image(systemName: "gear")
)
.toolbar(.visible, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarBackground(Color.brown, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline)
}
}
}
The result of this code looks like this App screenshot
How can I create a SwiftUI view with a tab bar positioned at the top of the screen, along with buttons on the leading and trailing side of tab bar, similar to the layout of the tab bar in Apple's Files app on iPad?
FYI, .navigationBarItems
is deprecated. You should use the replacement instead.
Although I am not sure exactly of the logic/setup used in the Files app, it does seem to use a TabView
with .sidebarAdaptable
style. This modifier is new to iOS 18 so you'll need to work in a project supporting iOS 18+.
When using multiple tabs, it's best for each tab to have its own NavigationStack
so you can control in which tab you want to navigate to a specific view. Then, each tab view or any of its children can contribute toolbar items to its navigation stack.
.navigationTitle("Browse")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
favoriteTab.toggle()
} label: {
Image(systemName: favoriteTab ? "heart.fill" : "heart")
}
}
}
If you want to have the same button available in all tabs (like the settings button), you can use a view extension function to define the toolbar item and then use that modifier on any view that should display a settings button in its navigation stack. In the example below, look at the .settingsToolbarItem
view extension.
.settingsToolbarItem(showNavigationArrows: true)
In this case, if the settings buttons is available in all tabs, the question becomes in which tab should the Settings view be displayed? For the purposes of the code below, the settings view should be displayed in the Home tab, which requires logic for controlling the selected tab from any view (or function).
In my experience, this is most easily achieved using a shared observable singleton, like the NavigationManager
class shown in the example code below, since you can access it or bind to it from anywhere without having to pass it to any or all views that may need it. This class also has properties for the navigation paths of each tab.
//Observable singleton
@Observable
class NavigationManager {
//Properties
var selectedTab: Int = 1
var homeNavigationPath: [NavigationRoute] = []
var browseNavigationPath: NavigationPath = NavigationPath()
var profileNavigationPath: NavigationPath = NavigationPath()
//Singleton
static let nav = NavigationManager()
private init() {}
}
How you want to go about the navigation paths is up to you, but as an example, I showed one way using a navigation route enum configured for the Home tab destinations:
//Navigation route enum
enum NavigationRoute {
case settings
var route: some View {
switch self {
case .settings: SettingsView()
}
}
}
... and the associated config in HomeTabView
:
.navigationDestination(for: NavigationRoute.self) { destination in
destination.route
}
With this setup, navigating to Settings in the Home tab becomes as simple as:
let nav = NavigationManager.nav
Button {
//Switch to home tab
nav.selectedTab = 1
//Navigate to settings using home tab's navigation stack
nav.homeNavigationPath.append(.settings)
} label: {
Image(systemName: "gear")
}
You can still use a NavigationLink
like in the Profile tab, for example:
.navigationTitle("Profile")
.toolbar {
ToolbarItem(placement: .primaryAction) {
NavigationLink {
VStack {
Text("Some content for adding a profile...")
}
.navigationTitle("Add profile")
} label: {
Label("Add profile", systemImage: "person.crop.circle.badge.plus")
}
}
}
import SwiftUI
import Observation
//Root view
struct ContentView: View {
//State values
@State private var nav: NavigationManager = NavigationManager.nav // <- initialize navigation manager singleton
//Body
var body: some View {
TabView(selection: $nav.selectedTab) {
Tab("Home", systemImage: "house", value: 1){
HomeTabView()
}
Tab("Browse", systemImage: "folder", value: 2){
BrowseTabView()
}
Tab("Profile", systemImage: "person.crop.circle", value: 3){
ProfileTabView()
}
}
.tabViewStyle(.sidebarAdaptable)
.tabViewSidebarHeader {
Text("Files")
.font(.largeTitle)
.fontWeight(.bold)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
//Home view
struct HomeTabView: View {
//Binding to observable navigation manager singleton
@Bindable var navigationManager = NavigationManager.nav
//Body
var body: some View {
NavigationStack(path: $navigationManager.homeNavigationPath) {
VStack {
ContentUnavailableView {
Label("No content", systemImage: "questionmark.circle.fill")
} description: {
Text("Nothing to show at this time.")
}
}
.navigationTitle("Home")
.settingsToolbarItem()
.navigationDestination(for: NavigationRoute.self) { destination in
destination.route
}
}
}
}
//Browse view
struct BrowseTabView: View {
//Binding to observable navigation manager singleton
@Bindable var navigationManager = NavigationManager.nav
//State values
@State private var favoriteTab = false
//Body
var body: some View {
NavigationStack(path: $navigationManager.browseNavigationPath) {
VStack {
ContentUnavailableView {
Label("No files", systemImage: "questionmark.folder.fill")
} description: {
Text("No files available.")
}
}
.navigationTitle("Browse")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
favoriteTab.toggle()
} label: {
Image(systemName: favoriteTab ? "heart.fill" : "heart")
}
}
}
.settingsToolbarItem(showNavigationArrows: true)
}
}
}
//Profile view
struct ProfileTabView: View {
//Binding to observable navigation manager singleton
@Bindable var navigationManager = NavigationManager.nav
//Body
var body: some View {
NavigationStack(path: $navigationManager.profileNavigationPath) {
VStack {
ContentUnavailableView {
Label("No profile", systemImage: "person.crop.circle.badge.questionmark.fill")
} description: {
Text("No user profile.")
}
}
.navigationTitle("Profile")
.toolbar {
ToolbarItem(placement: .primaryAction) {
NavigationLink {
VStack {
Text("Some content for adding a profile...")
}
.navigationTitle("Add profile")
} label: {
Label("Add profile", systemImage: "person.crop.circle.badge.plus")
}
}
}
}
}
}
//Settings view
struct SettingsView: View {
var body: some View {
VStack {
ContentUnavailableView {
Label("No settings", systemImage: "gear.badge.questionmark")
} description: {
Text("No settings configured.")
}
}
.navigationTitle("Settings")
.toolbar {
ToolbarItem(placement: .secondaryAction) {
Button {
//action here...
} label: {
Label("Reset settings", systemImage: "gearshape.arrow.trianglehead.2.clockwise.rotate.90")
}
}
}
}
}
//Navigation route enum
enum NavigationRoute {
case settings
var route: some View {
switch self {
case .settings: SettingsView()
}
}
}
//View extension
extension View {
func settingsToolbarItem(showNavigationArrows: Bool = false) -> some View {
self
.toolbar {
ToolbarItem(placement: .primaryAction) {
let nav = NavigationManager.nav
Button {
//Switch to home tab
nav.selectedTab = 1
//Navigate to settings using home tab's navigation stack
nav.homeNavigationPath.append(.settings)
} label: {
Image(systemName: "gear")
}
}
if showNavigationArrows {
ToolbarItemGroup(placement: .topBarLeading) {
Button {
//action here...
} label: {
Image(systemName: "chevron.left")
}
Button {
//action here...
} label: {
Image(systemName: "chevron.right")
}
.disabled(true)
}
}
}
}
}
//Observable singleton
@Observable
class NavigationManager {
//Properties
var selectedTab: Int = 1
var homeNavigationPath: [NavigationRoute] = []
var browseNavigationPath: NavigationPath = NavigationPath()
var profileNavigationPath: NavigationPath = NavigationPath()
//Singleton
static let nav = NavigationManager()
private init() {}
}
#Preview {
ContentView()
}