A SwiftUI Sidebar
Many apps use a sidebar, or menu, that appears when the user taps a control, typically in the upper left corner of the main screen. The menu slides in from the left and remains until dismissed somehow. This article shows you a very SwiftUI way of making a sidebar.
Let’s begin by making a Sidebar
component that wraps some content with a NavigationView
. Putting the content inside a NavigationView
gives the sidebar a title area and you can put other controls there if you like using the .toolbar
modifier (which I did not use for this example).
struct Sidebar<Content : View> : View { var title: LocalizedStringKey var content : Content init(title: LocalizedStringKey, @ViewBuilder content: () -> Content) { self.title = title self.content = content() } var body: some View { NavigationView { content .navigationTitle(title) } .navigationViewStyle(StackNavigationViewStyle()) } }
The Sidebar
view uses @ViewBuilder
and takes a title to display along with the content to wrap. You can use it like this:
Sidebar(title: "Main Menu") { List { Button("Home") { } Button("Actions") { } } }
Let’s give the Sidebar
an actual menu of items to display. Create a new SwiftUI View file called MenuItems.swift
like this:
struct MenuItems: View { enum MenuCode { case home case search case folder case edit case delete } var action: (MenuCode) -> Void var body: some View { List { Divider() Button(action: {self.action(.home)}) { Label("Home", systemImage: "house") } Button(action: {self.action(.search)}) { Label("Search", systemImage: "magnifyingglass") } Button(action: {self.action(.folder)}) { Label("Folder", systemImage: "folder") } Button(action: {self.action(.edit)}) { Label("Edit", systemImage: "pencil") } Divider() Button(action: {self.action(.delete)}) { Label("Delete", systemImage: "trash") } } .listStyle(SidebarListStyle()) } }
The menu is a list of buttons and tapping one calls the MenuItems
action
callback. You’ll see how to use this in a little bit.
Now you’ve got the sidebar itself squared away and can look at the bigger picture. What we want is to have the content of the app with a sidebar sliding over it to reveal the menu and sliding away once a menu item has been picked. Typically, when a sidebar is viewed, it does not cover the entire screen and the visible portion “below” the sidebar should not be enabled.
Basically you want this structure when the sidebar appears:
ZStack { // 1 - your main app content MainView() .zIndex(0) // 2 - a shield against using the main content Color.black .zIndex(1) .opacity(0.25) .edgesIgnoringSafeArea(.all) // 3 - SideBar with its menu SideBar(title: "Main Menu") { MenuItems() { menuItem in // do something with the menuItem } } .zIndex(2) .frame(width: 300) .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .leading))) }
The main content for your app.
The “shield” is just a translucent color that sits above your main content and covers the entire screen
The
Sidebar
itself with its content and action handler. It also has atransition
for when it is added and removed.
The code above, if used, would show the main content, the translucent black shield, and the Sidebar
. You want some control over whether or not the Sidebar
should appear. We can do that with a Bool
, an IF
statement, and a Button
to control the state of the Bool
.
Above the code in the last code block, insert this:
@State var presentSidebar = false func toggleSidebar() { withAnimation { self.presentSidebar.toggle() } }
When the toggleSidebar
function is called, it changes the state of the presentSidebar
but does so within an animation
block. Doing this will trigger the transition
applied to the Sidebar
and, as a bonus, animate any other changes on this screen. This includes the appearance of the color shield. Here is that code block once again, but with the presentSidebar
incorporated:
ZStack { // 1 - your main app content MainView() .zIndex(0) // 2 - a shield against using the main content if presentSideBar { Color.black .zIndex(1) .opacity(0.25) .edgesIgnoringSafeArea(.all) // 3 - SideBar with its menu SideBar(title: "Main Menu") { MenuItems() { menuItem in // do something with the menuItem self.toggleSidebar() } } .zIndex(2) .frame(width: 300) .transition(.asymmetric( insertion: .move(edge: .leading), removal: .move(edge: .leading)) ) } }
Now all you need is a button to toggle the value of presentSidebar
. You sidebar would slide in and when a menu item was picked, its action handler would flip the presentSidebar
toggle and cause the sidebar to slide away.
Before adding this button - which would just be temporary to show how this works - let’s make this block of code into a more generic container.
Create a new SwiftUI file called MainViewWithSidebar.swift
and put all of this into it:
struct MainViewWithSidebar <Sidebar, Content> : View where Sidebar : View, Content : View { @Binding var presentSidebar: Bool var sidebar : Sidebar var content : Content // 1 - using @ViewBuilder again init(presentSidebar: Binding<Bool>, sidebar: Sidebar, @ViewBuilder content: () -> Content) { self._presentSidebar = presentSidebar self.sidebar = sidebar self.content = content() } // use animation to change the presentSidebar value; this way // the menu will slide closed. private func toggleSidebar() { withAnimation { self.presentSidebar.toggle() } } var body: some View { ZStack(alignment: .leading) { // 2 - the main content of the app self.content .zIndex(0) .blur(radius: self.presentSidebar ? 6.0 : 0) // 3 - show or hide the translucent shield if presentSidebar { Color.black .zIndex(1) .opacity(0.25) .edgesIgnoringSafeArea(.all) .onTapGesture { self.toggleSidebar() } } // 4 - show or hide the sidebar itself if presentSidebar { self.sidebar .zIndex(2) .frame(width: 300) .transition(.asymmetric( insertion: .move(edge: .leading), removal: .move(edge: .leading)) ) } } } }
This looks pretty much like the code above it, except now it is its own View and uses @ViewBuilder
to take the content of the main view as well as the Sidebar
(which you’ll see how this is used below).
As with the
Sidebar
container, this container also uses@ViewBuilder
so you can use any View you like as content.self.content
is whereMainView()
was before. In addition to thezIndex
, there is also ablur
that depends on thepresentSidebar
boolean. Blurring the main content while the sidebar is visible is a nice touch and, using animation, SwiftUI will fade the blur effect.This is the Color shield. Here, a
onTapGesture
has been added as a way to make the sidebar disappear if the user taps outside of the sidebar.The sidebar presentation itself.
So how would you use this MainViewWithSidebar
component? Like this:
// 1 - boolean to track the visibility of the sidebar @State showSidebar = false // 2 - a way to communicate the sidebar menu choice to the MainView @State var selectedMenuItem = MenuItems.MenuCode.home // 3 - a tidy way to present the sidebar private var sidebarView: some View { Sidebar(title: "Main Menu") { MenuItems() { itemNumber in self.selectedMenuItem = itemNumber self.toggleSidebar() } } } var body: some View { // 4 - the container MainViewWithSidebar(presentSidebar: self.$showSidebar, sidebar: self.sidebarView) { NavigationView { MainView(menuCode: self.selectedMenuItem) // 5 - using .toolbar to add some controls .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button(action: {self.toggleSidebar()}) { Label("Menu", systemImage: "sidebar.left") } } } } } }
This is the boolean we’ll toggle to get the sidebar to appear. It gets passed as a binding to
MainViewWithSidebar
.This state variable gets set from the
MenuItems
component and is then passed down to theMainView
and used however it wants to use it.This
sidebarView
encapsulates the actualSidebar
container built at the beginning of this actual and wraps theMenuItems
.Now you see how to use the
MainViewWithSidebar
container. Pass in the binding for the boolean to show/hide the sidebar, and pass in the sidebar itself:sidebarView
.Finally, add
.toolbar
and place a button at the top to control the appearance of the sidebar. Thetoolbar
applies to theNavigationView
wrapping theMainView
.
So there you have it, a more or less, generic way to present a sidebar menu. Using @ViewBuilder
allows you to put any View as the sidebar as well as abstracting the mechanism of presenting the sidebar from the app itself. All you need to do is create your sidebar content and your application’s content, whatever that may be.