Sliding Menu from Top, Left and Right

Published by @SoNiceInfo at 6/24/2020


Various slide menu images

I'll show you how to create a sliding menu that appears from the top left and right with SwiftUI.
Implementing the menu and hamburger menu at the same time, which comes out from top, left and right sides by swiping. Swipe is implemented with gesture, hamburger menu is implemented with Button(action:). By combining the code introduced here, the slide menu can be displayed inside or outside of NavigationView.

To make the menu appear by swiping, the following code is important.
onChanged and onEnded detects when a screen operation is in progress and when it is finished.

.gesture(
    // Detects swipe-related events
    // If you set a minimumDistance, it will fire after a swipe of that distance is detected
    DragGesture(minimumDistance: 5)
        // Implements the movement when a swipe is detected
        .onChanged{ value in
            self.offset = v.translation.width
        }
        // Implement the movement when the swipe is finished
        .onEnded { value in
            if (value.translation.width > 0) {
                self.offset = self.openOffset
            } else {
                self.offset = self.closeOffset
            }
        }
)

Implementing a hamburger menu is very easy.
Implementing actions to open menu in Button(action:).

Button(action: {
    self.offset = .zero
}){
    Image(systemName: "list.bullet")
}

First, we need to prepare the menu. Copy and paste the following code to create a MenuView.swift.
To simplify the process, we use menu from Create a Twitter Home Screen and Slide Menu.
The swiping part is implemented in ContentView.swift.

//
//  MenuView.swift
//

import SwiftUI

struct MenuView: View {

    var body: some View {
        VStack(alignment: .leading) {
            Image("animal_kuma")
                .resizable()
                .overlay(
                    Circle().stroke(Color.gray, lineWidth: 1))
                .frame(width: 60, height: 60)
                .clipShape(Circle())
            Text("SwiftUIへの道")
                .font(.largeTitle)
            Text("@road2swiftui")
                .font(.caption)
            Divider()
            ScrollView (.vertical, showsIndicators: true) {
                HStack {
                    Image(systemName: "person")
                    Text("Profile")
                }
                HStack {
                    Image(systemName: "list.dash")
                    Text("Lists")
                }
                HStack {
                    Image(systemName: "text.bubble")
                    Text("Topics")
                }
            }
            Divider()
            Text("Settings and privacy")
        }
        .padding(.horizontal, 20)
    }
}


struct MenuView_Previews: PreviewProvider {
    static var previews: some View {
        MenuView()
    }
}
The screen where the MenuView was created

In DragGesture, if value.translation.height is a positive value, it is a top-to-bottom move.
Retriving start point of swiping with value.startLocation, menu will only appear if you swipe from the top of the screen. Place a hamburger menu in navigationBarItems.

//
//  ContentView.swift
//

import SwiftUI

struct ContentView: View {
    // The offset variable holds the offset to show or hide the menu.
    @State private var offset = CGFloat.zero
    @State private var closeOffset = CGFloat.zero
    @State private var openOffset = CGFloat.zero
    
    var body: some View {
        // Use GeometoryReader to get the screen size
        GeometryReader { geometry in
            NavigationView {
                ZStack(alignment: .topLeading) {
                    // Main Content
                    VStack () {
                        Spacer()
                        Text("This is main contents")
                            .font(.largeTitle)
                        Spacer()
                    }
                    .background(Color.white)
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
                    // Gray out the main content when the slide menu comes up
                    Color.gray.opacity(
                        Double((self.closeOffset - self.offset)/self.closeOffset) - 0.4
                    )
                    // Slide menu
                    MenuView()
                        .background(Color.white)
                        .frame(width: geometry.size.width, height: geometry.size.height * 0.7)
                        // First, minus the value of the offset for screen width for the slide menu.
                        .onAppear(perform: {
                            self.offset = (geometry.frame(in: .global).origin.y + geometry.size.height) * -1
                            self.closeOffset = self.offset
                            self.openOffset = .zero
                        })
                        .offset(y: self.offset)
                        // Set the animation of the slide.
                        .animation(.default)
                }
                // Implementing the gestures
                // Set swipe thresholds to prevent users from unexpectedly appearing in menus
                .gesture(DragGesture(minimumDistance: 5)
                    .onChanged{ value in
                        // Use startLocation to keep the main content from scrolling vertically
                        // Reduce the value of the offset (menu position) according to the distance swiped
                        if (self.offset != self.openOffset && value.startLocation.y < 30) {
                            self.offset = self.closeOffset + value.translation.height
                        }
                    }
                    .onEnded { value in
                        // Open the menu if the end position is lower than the swipe start position.
                        if (value.startLocation.y < value.location.y) {
                            if (value.startLocation.y < 30) {
                                self.offset = self.openOffset
                            }
                        } else {
                            self.offset = self.closeOffset
                        }
                    }
                )
                // Set the items to be displayed on the top NavigationBar
                .navigationBarTitle("This is bar", displayMode: .inline)
                .navigationBarItems(trailing: Button(action: {
                        if (self.offset == self.openOffset) {
                            self.offset = self.closeOffset
                        } else {
                            self.offset = self.openOffset
                        }
                    }){
                        Image(systemName: "list.bullet")
                    })
                    .edgesIgnoringSafeArea(.bottom)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Slide menu from the topSlide menu from the top

In DragGesture, if value.translation.width is a positive value, it is a left-to-right move.

//
//  ContentView.swift
//

import SwiftUI

struct ContentView: View {
    // The offset variable holds the offset to show or hide the menu.
    @State private var offset = CGFloat.zero
    @State private var closeOffset = CGFloat.zero
    @State private var openOffset = CGFloat.zero
    
    var body: some View {
        // Use GeometoryReader to get the screen size
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                // Main Content
                VStack () {
                    HStack () {
                        Button(action: {
                            self.offset = self.openOffset
                        }){
                            Image(systemName: "list.bullet")
                        }
                        Spacer()
                        Text("This Is Bar")
                        Spacer()
                    }
                    .padding(.horizontal)
                    Divider()
                    Spacer()
                    Text("This is main contents")
                        .font(.largeTitle)
                    Spacer()
                }
                .background(Color.white)
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
                // Gray out the main content when the slide menu comes up
                Color.gray.opacity(
                    Double((self.closeOffset - self.offset)/self.closeOffset) - 0.4
                )
                // Slide menu
                MenuView()
                    .background(Color.white)
                    .frame(width: geometry.size.width * 0.7)
                    // First, minus the value of offset for the screen width for the slide menu.
                    .onAppear(perform: {
                        self.offset = geometry.size.width * -1
                        self.closeOffset = self.offset
                        self.openOffset = .zero
                    })
                    .offset(x: self.offset)
                    // Set the animation of the slide.
                    .animation(.default)
            }
            // Implementing the gestures
            // Set swipe thresholds to prevent users from unexpectedly appearing in menus
            .gesture(DragGesture(minimumDistance: 5)
                .onChanged{ value in
                    // Reduce the value of the offset (menu position) according to the distance swiped
                    if (self.offset < self.openOffset) {
                        self.offset = self.closeOffset + value.translation.width
                    }
                }
                .onEnded { value in
                    // Open the menu if the end swipe position is to the right of the start position.
                    if (value.location.x > value.startLocation.x) {
                        self.offset = self.openOffset
                    } else {
                        self.offset = self.closeOffset
                    }
                }
            )
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Here is the codes for using NavigationView.

//
//  ContentView.swift
//

import SwiftUI

struct ContentView: View {
    // The offset variable holds the offset to show or hide the menu.
    @State private var offset = CGFloat.zero
    @State private var closeOffset = CGFloat.zero
    @State private var openOffset = CGFloat.zero
    
    var body: some View {
        // Use GeometoryReader to get the screen size
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                // Main Content
                NavigationView {
                    ZStack {
                        VStack () {
                            Spacer()
                            Text("This is main contents")
                                .font(.largeTitle)
                            Spacer()
                        }
                        // Gray out the main content when the slide menu comes up
                        Color.gray.opacity(
                            Double((self.closeOffset - self.offset)/self.closeOffset) - 0.4
                        )
                    }
                    .navigationBarTitle("This is bar", displayMode: .inline)
                    .navigationBarItems(leading: Button(action: {
                            self.offset = self.openOffset
                        }){
                            Image(systemName: "list.bullet")
                        })
                    .edgesIgnoringSafeArea(.vertical)
                }
                // Slide menu
                MenuView()
                    .background(Color.white)
                    .frame(width: geometry.size.width * 0.7)
                    .edgesIgnoringSafeArea(.bottom)
                    // First, minus the value of offset for the screen width for the slide menu.
                    .onAppear(perform: {
                        self.offset = geometry.size.width * -1
                        self.closeOffset = self.offset
                        self.openOffset = .zero
                    })
                    .offset(x: self.offset)
                    // Set the animation of the slide.
                    .animation(.default)
            }
            // We'll implement the gestures
            // Set swipe thresholds to prevent users from unexpectedly appearing in menus
            .gesture(DragGesture(minimumDistance: 5)
                    .onChanged{ value in
                        // Reduce the value of the offset (menu position) according to the distance swiped
                        if (self.offset < self.openOffset) {
                            self.offset = self.closeOffset + value.translation.width
                        }
                    }
                    .onEnded { value in
                        // If the end of the swipe is to the right of the start position, open the menu.
                        if (value.location.x > value.startLocation.x) {
                            self.offset = self.openOffset
                        } else {
                            self.offset = self.closeOffset
                        }
                    }
                )
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Slide menu from the leftSlide menu from the left

In DragGesture, if value.translation.width is a negative value, it is a right-to-left move.

//
//  ContentView.swift
//

import SwiftUI

struct ContentView: View {
    // The offset variable holds the offset to show or hide the menu.
    @State private var offset = CGFloat.zero
    @State private var closeOffset = CGFloat.zero
    @State private var openOffset = CGFloat.zero
    
    var body: some View {
        // Use GeometoryReader to get the screen size
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                // Main Content
                VStack () {
                    HStack () {
                        Spacer()
                        Text("This Is Bar")
                        Spacer()
                        Button(action: {
                            self.offset = self.openOffset
                        }){
                            Image(systemName: "list.bullet")
                        }
                    }
                    .padding(.horizontal)
                    Divider()
                    Spacer()
                    Text("This is main contents")
                        .font(.largeTitle)
                    Spacer()
                }
                .background(Color.white)
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
                // Gray out the main content when the slide menu comes up
                Color.gray.opacity(
                    Double((self.closeOffset - self.offset)/self.closeOffset) - 0.4
                )
                // Slide menu
                MenuView()
                    .background(Color.white)
                    .frame(width: geometry.size.width * 0.7)
                    // First, minus the value of offset for the screen width for the slide menu.
                    .onAppear(perform: {
                        self.offset = geometry.size.width
                        self.closeOffset = self.offset
                        self.openOffset = geometry.size.width - geometry.size.width * 0.7
                    })
                    .offset(x: self.offset)
                    // Set the animation of the slide.
                    .animation(.default)
            }
            // Implementing the gestures
            // Set swipe thresholds to prevent users from unexpectedly appearing in menus
                .gesture(DragGesture(minimumDistance: 5)
                    .onChanged{ value in
                        // Reduce the value of the offset (menu position) according to the distance swiped
                        if (self.offset > self.openOffset) {
                            self.offset = self.closeOffset + value.translation.width
                        }
                    }
                    .onEnded { value in
                        // If the distance traveled is in the negative direction, open the menu
                        if (value.translation.width < 0) {
                            self.offset = self.openOffset
                        } else {
                            self.offset = self.closeOffset
                        }
                    }
                )
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Slide menu from the leftSlide menu from the left

Download

Codes in this article is published on GitHub.
https://github.com/d1v1b/Various-Slide-Menus

Note

Here's how to create a card-based sliding menu at the bottom of the screen, which is used in the Maps and Google apps.
Card Style Sliding Menu from Bottom



    I released iOS App!

    ToDo App

    Visualize Activity, Data sharing with iCloud, Dark mode supported.

    リリースしたToDoアプリのスクリーンショット

    IP Address bookmark.

    Check and bookamrk IP address of all interfaces with geolocation.

    リリースしたIPアドレス保存アプリのスクリーンショット