Creating a Side Menu Navigation Drawer in SwiftUI

Navigation is a cornerstone of any well-designed mobile application. In SwiftUI, a popular method for navigation is the side menu, also known as a navigation drawer. This pattern allows users to access primary app features from anywhere within the application.

What is a Side Menu Navigation Drawer?

A side menu navigation drawer is a panel that slides in from the side (usually the left) of the screen to reveal navigation options. It is typically triggered by a ‘hamburger’ menu icon. This pattern provides a compact and accessible way to expose main features without cluttering the primary screen.

Why Use a Side Menu?

  • Clean UI: Keeps the primary interface uncluttered.
  • Easy Access: Provides quick access to main navigation items from anywhere in the app.
  • Consistent Experience: Offers a consistent navigation pattern across different screens.

How to Implement a Side Menu in SwiftUI

Creating a side menu in SwiftUI involves several key steps:

Step 1: Create the Basic Structure

Start by creating a struct that will represent your main content view, which will house the side menu.

import SwiftUI

struct ContentView: View {
    @State private var isShowingMenu = false

    var body: some View {
        NavigationView {
            ZStack {
                if isShowingMenu {
                    SideMenuView(isShowing: $isShowingMenu)
                }
                
                MainView()
                    .cornerRadius(isShowingMenu ? 20 : 0)
                    .scaleEffect(isShowingMenu ? 0.8 : 1)
                    .offset(x: isShowingMenu ? 200 : 0)
                    .disabled(isShowingMenu) // Disables interaction with main view
                
                // Hamburger Button
                if !isShowingMenu {
                    Button(action: {
                        withAnimation(.spring()) {
                            isShowingMenu.toggle()
                        }
                    }) {
                        Image(systemName: "line.horizontal.3")
                            .foregroundColor(.black)
                            .font(.title)
                    }
                    .padding()
                    .frame(maxWidth: .infinity, alignment: .topLeading)
                }
            }
            .navigationTitle("My App")
            .navigationBarHidden(true) // Hide default nav bar to use our own
        }
    }
}

Explanation:

  • @State private var isShowingMenu = false: A state variable that controls the visibility of the side menu.
  • ZStack: Used to overlay the side menu and the main content.
  • SideMenuView: The view that holds the content of the side menu. Will be defined in the next steps
  • MainView: The primary content view that is shown when the menu is hidden. Defined in the next steps.
  • Modifiers on MainView like cornerRadius, scaleEffect, and offset create an appealing animation when the menu is toggled.
  • A Button with a hamburger icon toggles the visibility of the side menu.
  • navigationBarHidden(true) ensures that the default navigation bar is hidden, as we’re handling the navigation within our view.

Step 2: Create the SideMenuView

Create the SideMenuView which contains the navigation options.

import SwiftUI

struct SideMenuView: View {
    @Binding var isShowing: Bool
    var body: some View {
        ZStack {
            LinearGradient(gradient: Gradient(colors: [Color.blue, Color.purple]), startPoint: .top, endPoint: .bottom)
                .ignoresSafeArea()
            
            VStack(alignment: .leading, spacing: 20) {
                // Profile Header
                HStack {
                    Image(systemName: "person.circle.fill")
                        .font(.largeTitle)
                        .foregroundColor(.white)
                    Text("User Name")
                        .font(.headline)
                        .foregroundColor(.white)
                }
                .padding(.bottom, 30)

                // Menu Options
                SideMenuOptionView(option: .profile)
                SideMenuOptionView(option: .settings)
                SideMenuOptionView(option: .logout)
                
                Spacer()
            }
            .padding()
            .frame(maxWidth: .infinity, alignment: .leading)
            
            
        }
        .frame(width: 280)
        .offset(x: isShowing ? 0 : -280, y: 0)
        .transition(.move(edge: .leading))

    }
}

struct SideMenuView_Previews: PreviewProvider {
    static var previews: some View {
        SideMenuView(isShowing: .constant(true))
    }
}

Explanation:

  • A ZStack is used to create a background gradient using LinearGradient.
  • A VStack aligns the menu items to the leading edge with spacing.
  • A header with a profile icon and user name.
  • Three instances of the custom SideMenuOptionView are created (Profile, Settings, Logout). Defined in the next steps.
  • A spacer pushes content to the top.
  • The width is set to 280 points.
  • The view’s x-offset is animated based on the isShowing state. When isShowing is false (menu hidden), the view is offset to the left by -280 points.
  • A .transition(.move(edge: .leading)) applies a transition effect to the menu when it appears or disappears.

Step 3: Define SideMenu Options

Create an enum to handle available options to keep the code clean

enum SideMenuOptions: String, CaseIterable {
    case profile = "Profile"
    case settings = "Settings"
    case logout = "Logout"
    
    var imageName: String {
        switch self {
        case .profile:
            return "person"
        case .settings:
            return "gear"
        case .logout:
            return "arrow.left.square"
        }
    }
}

Explanation:

  • Define an enum `SideMenuOptions` with available options such as `profile`, `settings`, and `logout`.
  • Use computed properties like `imageName` to map each case to a relevant system icon, making the option items easier to manage and display.

Step 4: Implement the Side Menu Option View

The SideMenuOptionView contains the UI for an individual option within the side menu

import SwiftUI

struct SideMenuOptionView: View {
    
    let option: SideMenuOptions
    var body: some View {
        HStack(spacing: 15) {
            Image(systemName: option.imageName)
                .font(.headline)
                .foregroundColor(.white)
            
            Text(option.rawValue)
                .foregroundColor(.white)
                .font(.subheadline)
            
            Spacer()
        }
        .padding(.top, 12)
        .frame(height: 40)
    }
}

Explanation:

  • Create a custom view named `SideMenuOptionView`, designed to encapsulate the layout of a single menu option item within a side menu.
  • The view is initialized with an option from the `SideMenuOptions` enum, enabling customization of the item based on its type (e.g., Profile, Settings, Logout).
  • An `HStack` arranges an image and a text label horizontally with some spacing in between. The image displays a system icon that corresponds to the type of menu item.
  • Customize `foregroundColor` to give more focus to menu texts and their icons and change `font` styles with `.subheadline`.

Step 5: Implement the Main Content View

Finally, create the main view that represents your main application screen:

import SwiftUI

struct MainView: View {
    var body: some View {
        VStack {
            Text("Main Content View")
                .font(.largeTitle)
                .padding()
            
            Text("This is where your main app content goes.")
                .font(.body)
                .padding()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.gray.opacity(0.1))
    }
}

struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}

Explanation:

  • Create a `VStack` to contain the content.
  • Display some sample text within the content view.
  • Customize the frame and background for better presentation.

Complete Example

Putting all the code together for a full picture.

import SwiftUI

enum SideMenuOptions: String, CaseIterable {
    case profile = "Profile"
    case settings = "Settings"
    case logout = "Logout"
    
    var imageName: String {
        switch self {
        case .profile:
            return "person"
        case .settings:
            return "gear"
        case .logout:
            return "arrow.left.square"
        }
    }
}

struct SideMenuOptionView: View {
    
    let option: SideMenuOptions
    var body: some View {
        HStack(spacing: 15) {
            Image(systemName: option.imageName)
                .font(.headline)
                .foregroundColor(.white)
            
            Text(option.rawValue)
                .foregroundColor(.white)
                .font(.subheadline)
            
            Spacer()
        }
        .padding(.top, 12)
        .frame(height: 40)
    }
}

struct SideMenuView: View {
    @Binding var isShowing: Bool
    var body: some View {
        ZStack {
            LinearGradient(gradient: Gradient(colors: [Color.blue, Color.purple]), startPoint: .top, endPoint: .bottom)
                .ignoresSafeArea()
            
            VStack(alignment: .leading, spacing: 20) {
                // Profile Header
                HStack {
                    Image(systemName: "person.circle.fill")
                        .font(.largeTitle)
                        .foregroundColor(.white)
                    Text("User Name")
                        .font(.headline)
                        .foregroundColor(.white)
                }
                .padding(.bottom, 30)

                // Menu Options
                SideMenuOptionView(option: .profile)
                SideMenuOptionView(option: .settings)
                SideMenuOptionView(option: .logout)
                
                Spacer()
            }
            .padding()
            .frame(maxWidth: .infinity, alignment: .leading)
            
            
        }
        .frame(width: 280)
        .offset(x: isShowing ? 0 : -280, y: 0)
        .transition(.move(edge: .leading))

    }
}

struct MainView: View {
    var body: some View {
        VStack {
            Text("Main Content View")
                .font(.largeTitle)
                .padding()
            
            Text("This is where your main app content goes.")
                .font(.body)
                .padding()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.gray.opacity(0.1))
    }
}

struct ContentView: View {
    @State private var isShowingMenu = false

    var body: some View {
        NavigationView {
            ZStack {
                if isShowingMenu {
                    SideMenuView(isShowing: $isShowingMenu)
                }
                
                MainView()
                    .cornerRadius(isShowingMenu ? 20 : 0)
                    .scaleEffect(isShowingMenu ? 0.8 : 1)
                    .offset(x: isShowingMenu ? 200 : 0)
                    .disabled(isShowingMenu) // Disables interaction with main view
                
                // Hamburger Button
                if !isShowingMenu {
                    Button(action: {
                        withAnimation(.spring()) {
                            isShowingMenu.toggle()
                        }
                    }) {
                        Image(systemName: "line.horizontal.3")
                            .foregroundColor(.black)
                            .font(.title)
                    }
                    .padding()
                    .frame(maxWidth: .infinity, alignment: .topLeading)
                }
            }
            .navigationTitle("My App")
            .navigationBarHidden(true) // Hide default nav bar to use our own
        }
    }
}

Conclusion

A side menu navigation drawer provides an intuitive and efficient navigation experience in iOS applications. SwiftUI simplifies the implementation of this pattern, allowing developers to create clean, accessible UIs with minimal code. By following these steps, you can create a functional and visually appealing side menu that enhances the user experience in your SwiftUI app.