Sliding Menu from Top, Left and Right
Published by @SoNiceInfo at 6/24/2020
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
.
Key to Sliding Menu
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
}
}
)
Key to Hamburger Menu
Implementing a hamburger menu is very easy.
Implementing actions to open menu in Button(action:)
.
Button(action: {
self.offset = .zero
}){
Image(systemName: "list.bullet")
}
Prepare Menu
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()
}
}
Menu from Top
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()
}
}
Menu from Left
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()
}
}
Menu from Right
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()
}
}
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