How to create a side menu (hamburger menu) in SwiftUI

Share on facebook
Share on twitter
Share on pinterest

Welcome to a new SwiftUI tutorial! In this article, we will learn how to create a side menu with a smooth slide-out animation, also called hamburger menu, in SwiftUI. These kinds of menus are often used in Android apps, but can also be useful in iOS apps as an alternative or addition to tab bars.

Here’s what we are going to achieve in this tutorial:

Setting up the main view 👨‍💻

The first thing we do is to create the main view of our app. This view should only contain a button which we will use to toggle the side menu later. Therefore, add a Button with the corresponding Text to the ContentView.

Button(action: {
   print("Open the side menu")
}) {
       Text("Show Menu")
    }

Let’s outsource this by CMD-clicking on the Button and selecting “Extract as subview”. Name the outsourced view MainView.

struct ContentView: View {
    var body: some View {
        MainView()
    }
}

struct MainView: View {
    var body: some View {
        Button(action: {
            print("Open the side menu")
        }) {
            Text("Show Menu")
        }
    }
}

The MainView should fill the whole screen. In order to know the height and width of the overall super view, we wrap the MainView into a GeometryReader.

GeometryReader { geometry in
            MainView()
        }

We can now use the geometry property to adjust the frame of our MainView to fill the entire screen.

MainView()
  .frame(width: geometry.size.width, height: geometry.size.height)

Your preview should now look like this:

Designing the menu 👨‍🎨

Now we will design our side menu. Create a new File-New-File, create a SwiftUI view and call it MenuView. Our menu should contain four vertically arranged menu items. For this purpose, we use a VStack with .leading as the alignment mode.

struct MenuView: View {
    var body: some View {
        VStack(alignment: .leading) {
            
        }
    }
}

Each menu item consists of an icon and a corresponding text. Thus, we add an HStack to our VStack for our first menu item.

VStack(alignment: .leading) {
            HStack {
                
            }
        }

Now we use the “person” system icon, which we make gray and enlarged.

HStack {
    Image(systemName: "person")
        .foregroundColor(.gray)
        .imageScale(.large)
    }

Then, we add the corresponding text:

HStack {
    Image(systemName: "person")
        .foregroundColor(.gray)
        .imageScale(.large)
    Text("Profile")
        .foregroundColor(.gray)
        .font(.headline)
        }

The first menu item should have a fairly large distance to the top. To do this we use the .padding modifier for the HStack.

HStack {
          //...
            }
                .padding(.top, 100)

We repeat these steps for the other menu items, but with other icons, texts and paddings.

VStack(alignment: .leading) {
            HStack {
                Image(systemName: "person")
                    .foregroundColor(.gray)
                    .imageScale(.large)
                Text("Profile")
                    .foregroundColor(.gray)
                    .font(.headline)
            }
                .padding(.top, 100)
            HStack {
                Image(systemName: "envelope")
                    .foregroundColor(.gray)
                    .imageScale(.large)
                Text("Messages")
                    .foregroundColor(.gray)
                    .font(.headline)
            }
                .padding(.top, 30)
            HStack {
                Image(systemName: "gear")
                    .foregroundColor(.gray)
                    .imageScale(.large)
                Text("Settings")
                    .foregroundColor(.gray)
                    .font(.headline)
            }
                .padding(.top, 30)
        }

Finally, we push our entire menu up by adding a Spacer.

VStack(alignment: .leading) {
            HStack {
                //...
            }
                .padding(.top, 100)
            HStack {
                //...
            }
            .padding(.top, 30)
            HStack {
                //....
            }
            .padding(.top, 30)
            Spacer()
        }

To increase the distance between the menu items and the edges, we apply some overall padding to the entire VStack.

VStack(alignment: .leading) {
            //...
        }
            .padding()

We also want our menu to be as wide as possible and the individual items to be aligned to the left.

VStack(alignment: .leading) {
            //...
        }
            .padding()
            .frame(maxWidth: .infinity, alignment: .leading)

We finally give our menu a dark grey background and make sure that it also goes beyond the so-called Safe Area, meaning that it also fills out the upper and lower edges.

VStack(alignment: .leading) {
            //...
        }
            .padding()
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(Color(red: 32/255, green: 32/255, blue: 32/255))
            .edgesIgnoringSafeArea(.all)

Great, we’re done creating our MenuView. The corresponding preview should now look like this:

Inserting the MenuView into our ContentView ➡️

To keep track of whether the MenuView should be displayed inside our ContentView or not, we declare an according State:

struct ContentView: View {
    
    @State var showMenu = false
    
    var body: some View {
        //...
    }
}

When this State is true, the MenuView should be shown on top of our MainView, aligned on the left side. Therefore, we wrap the MainView into a ZStack und insert the MenuView depending on the State’s value.

ZStack(alignment: .leading) {
                MainView()
                    .frame(width: geometry.size.width, height: geometry.size.height)
                if self.showMenu {
                    MenuView()
                }
            }

The menu should only cover the half of our screen, therefore we add the following .frame:

if self.showMenu {
    MenuView()
        .frame(width: geometry.size.width/2)
    }

We can now create a Binding from our MainView to our showMenu State …

struct MainView: View {
    
    @Binding var showMenu: Bool
    
    var body: some View {
        //...
    }
}

… and initialise it inside our ContentView:

MainView(showMenu: self.$showMenu)
    .frame(width: geometry.size.width, height: geometry.size.height)

We can now use our MainView’s button to toggle the showMenu State through it’s Binding which causes the ContentView to rebuild itself with eventually showing our MenuView

Button(action: {
            self.showMenu = true
        }) {
            Text("Show Menu")
        }

Additionally, we want to shift our MainView to the right when the side menu is opened. We also want to disable any functionality of our MainView until the menu is closed again.

MainView(showMenu: self.$showMenu)
    .frame(width: geometry.size.width, height: geometry.size.height)
    .offset(x: self.showMenu ? geometry.size.width/2 : 0)
    .disabled(self.showMenu ? true : false)

Run the app preview in live mode to try it out! Awesome, when we tap on the “Show Menu” button, our side menu gets displayed to us. At the same time, our MainView shifts to the right and the button it contains is grayed out.

So far, however, our menu gets still displayed without any kind of animation. Instead, we want to use a “slide-in” transition.

Therefore, we wrap the action of the button in our MainView into a withAnimation statement.

Button(action: {
            withAnimation {
               self.showMenu = true
            }
        }) {
            Text("Show Menu")
        }

Now we can attach a transition modifier to our MenuView and specify that the menu should move in from the left side.

MenuView()
    .frame(width: geometry.size.width/2)
    .transition(.move(edge: .leading))

If you now run the app again, you will see that the menu appears with a slide-in transition.

Hint: If the animation is not being executed in the live preview, run the app in the regular simulator.

Swipe to close the menu 👆

We want to be able to close the menu again by swiping from right to left. For this purpose, we use a so-called Drag Gesture. We create such a gesture by declaring it within our ContentView’s body and marking the remaining view’s content with the return keyword.

var body: some View {
        
        let drag = DragGesture()
        
         return GeometryReader { geometry in
            //...
        }
    }

When we have swiped far enough we want to close our menu by setting the showMenu State to false again. To do this we use the .onEnded modifier for our drag gesture.

let drag = DragGesture()
            .onEnded {
                
        }

In this, we check whether the user has exceeded a certain threshold value with his swiping gesture. If this is the case, we close the menu.

let drag = DragGesture()
            .onEnded {
                if $0.translation.width < -100 {
                    withAnimation {
                        self.showMenu = false
                    }
                }
            }

We can now simply attach the created gesture to our ContentView.

ZStack(alignment: .leading) {
                //...
            }
                .gesture(drag)

If we now run the app and have the menu opened, we can simply swipe to the left to let the menu collapse.

Implementing the Burger Button 🍔

Finally, we would also like to have the possibility to open and close the menu using a so-called hamburger icon. For this purpose, we wrap the GeometryReader of our ContentView into a NavigationView and add a navigation bar title to it.

return NavigationView {
            GeometryReader { geometry in
                //...
            }
                .navigationBarTitle("Side Menu", displayMode: .inline)
        }

We can now add an item to the left side of our navigation bar by using the .navigationBarItems modifier with the leading argument.

.navigationBarTitle("Side Menu", displayMode: .inline)
.navigationBarItems(leading: (
))

In this, we now add a suitable system icon and wrap it into a button with which we toggle the showMenu State.

.navigationBarItems(leading: (
                    Button(action: {
                        withAnimation {
                            self.showMenu.toggle()
                        }
                    }) {
                        Image(systemName: "line.horizontal.3")
                            .imageScale(.large)
                    }
                ))

If we execute the app now we can open and close our side menu with the MainView’s button and the swipe gesture as well as with the “hamburger” icon in the navigation bar.

Conclusion 🎊

That’s it! In this tutorial we learned how to add a so-called Hamburger/Side Menu to a SwiftUI app. We also saw how to let it move in with an appropriate transition and how to close it with a swipe gesture.

You can find the complete source code for the app here.

If you want to learn more about SwiftUI, check out our other tutorials! Also make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials and tips about SwiftUI and more!

16 replies on “How to create a side menu (hamburger menu) in SwiftUI”

How do I get to have the menu slide wider than the current setting? I would like it to slide out almost to the edge of the screen.

Last question, if you wanted the slideout menu to shift the navigation bar as well instead of keeping it in place as is now, how could that be achieved?

Hi, thanks a lot, this really helped! Can I ask how you would make it so that you can press on the icons and change the page whilts still having the possibility to open the menu?

let drag = DragGesture()
.onEnded {
if $0.translation.width 70 {
withAnimation {
self.showMenu = true
}
}
}

I tried to implement opening the side menu with drag gesture(from left to right to open it) but it doesn’t work… is there something else that should be added?

Hi syd, you missed the return NavigationView { GeometryReader {…} }, thats not pointed out explicitly, see below. You get the App on GitHub for review:

https://github.com/BLCKBIRDS/Side-Menu–Hamburger-Menu–in-SwiftUI

var body: some View {

let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
withAnimation {
self.showMenu = false
}
}
}

return NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
MainView(showMenu: self.$showMenu)
.frame(width: geometry.size.width, height: geometry.size.height)
.offset(x: self.showMenu ? geometry.size.width/2 : 0)
.disabled(self.showMenu ? true : false)
if self.showMenu {
MenuView()
.frame(width: geometry.size.width/2)
.transition(.move(edge: .leading))
}
}
.gesture(drag)
}
.navigationBarTitle("Side Menu", displayMode: .inline)
.navigationBarItems(leading: (
Button(action: {
withAnimation {
self.showMenu.toggle()
}
}) {
Image(systemName: "line.horizontal.3")
.imageScale(.large)
}
))
}
}

Thank you!

How do I pass in a binding in the preview?
struct StaffMemberDetail_Previews: PreviewProvider {
static var previews: some View {
let staffMember = StaffMember(id: 0, name: “testStaff”, code: “1”)
return StaffMemberDetail(staffMember: staffMember, showMenu: <#T##Binding#>)
}
}

Nice Tutorial!
How would you use the HStack menu item to display another View?
I have tried to use the .onTapGesture of the HStack, but can’t figure out how to show a view since it is integrated into the ContentView. Thanks in advance

Hi, Love this tutorial but I’m struggling to get this integrated with my project.
I want a side menu on my content view, the main stay of which is a ListView(). I can’t figure how to incorporate MainView properly. The best I have got so far is the menu appears and fills the bottom left corner on my content view. Anything else I’ve tried ends up not displaying the menu at all.

Adding one of the side menu views to the example would be good.
i.e. ProfileView() that when the menu item is pressed is goes to that view with the side menu still in place.

P.S. I already have both you Swift UI e Books 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount

small_c_popup.png

Are you ready for a new era of iOS development?

Start learning swiftUI today - download our free e-book