SwiftUI Menu Widget (easy to customize & integrate)

Madison Gipson
May 29, 2020 · 7 min read
Home Screen with Menu Widget.

I was tired of seeing & using the same old tab view navigation in apps, so I created a menu widget that mixes up navigation a little, without making it overly complicated or confusing. This tutorial outlines how I created this from scratch and how you can easily integrate it into your iOS app. While it’s built in SwiftUI, there are ways to integrate it with UIKit views (tutorial on coming later). If you just want the finished product, here’s the GitHub repo.

Start from Scratch

Let’s dive in, starting with a blank Single View App XCode project.

Use SwiftUI and don’t include tests.

Once created, rename ContentView.swift to ViewController.swift; there will be a couple instances in that file and SceneDelegate.swift where you will need to change ContentView to ViewController. You don’t need to rename, I just find it helps keep things straight when working in larger projects.

Because this is started from scratch, we need to add in some SwiftUI files that will be our different views to display. I created ViewA, ViewB, and ViewC; each have a simple line of text with their view name.

Shows ViewA. Same was done in ViewB and ViewC to keep it simple.

Back in ViewController, I removed the PreviewProvider since I usually just use the simulator to test, and added the following, which results in a plain white screen with “View A” in the center.

struct ViewController: View { 
@State var page:String = "ViewA"
var body: some View {
VStack {
if page == "ViewA" { ViewA() }
if page == "ViewB" { ViewB() }
if page == "ViewC" { ViewC() }
} //end of page vstack

} //end of view
} //end of struct

Switching Screens

There’s no way to switch between screens yet, so that’s what we’ll add next. In the code snippet below, this is what’s implemented:

  • Set screenSize & iconSize (set iconSize relative to screenSize for scaling).
  • Below the page VStack, we add a ZStack that contains a VStack of buttons; the nesting of a VStack inside a ZStack seems redundant, but the frame with .bottomTrailing alignment (necessary for menu placement in bottom right corner instead of center screen default) does not have the same effect if applied to just the button VStack.
  • Group the page and button stacks together in a ZStack so that the buttons are layered over the page.
struct ViewController: View {
@State var page:String = "ViewA"
let screenSize = UIScreen.main.bounds
let iconSize = UIScreen.main.bounds.width*0.07


var body: some View {
ZStack {
VStack {
if page == "ViewA" { ViewA() }
if page == "ViewB" { ViewB() }
if page == "ViewC" { ViewC() }
} //end of page vstack
ZStack {
VStack {
Button(action: { self.page = "ViewA" })
{
Image(systemName: "a.circle.fill").resizable().frame(width: iconSize, height: iconSize)
}
Button(action: { self.page = "ViewB" })
{
Image(systemName: "b.circle.fill").resizable().frame(width: iconSize, height: iconSize)
}
Button(action: { self.page = "ViewC" })
{
Image(systemName: "c.circle.fill").resizable().frame(width: iconSize, height: iconSize)
}
}.padding([.all]) //end of button vstack
}.frame(width: screenSize.width, height: screenSize.height, alignment: .bottomTrailing) //end of button zstack

} //end of view zstack
} //end of view
} //end of struct
Success!

Functional != Fun to Use

Cool, switching screens works! But there are still several things to fix before this is actually usable.

  • Position menu on screen correctly
  • Give the button style a facelift
  • Add a “trigger” to expand and collapse the menu so we don’t see all the buttons constantly (will get in the way of the background screen)

Button Style

We can kill the first two birds with one stone by creating a button style (I stuck to something incredibly simple for this example, but it demonstrates creating a reusable, uniform button style). Above the ViewController struct, add:

struct PageButtonStyle: ButtonStyle { 
let buttonSize = UIScreen.main.bounds.width*0.12
func makeBody(configuration: Self.Configuration) -> some View {
return configuration.label
.foregroundColor(Color.white)
.frame(width: buttonSize, height: buttonSize)
.background(Color.green)
.scaleEffect(configuration.isPressed ? 0.9 : 1.0)
}
}

Now append the button style to each button in the VStack like this:

Button(action: { self.page = "ViewA" }) 
{
Image(systemName: "a.circle.fill").resizable().frame(width: iconSize, height: iconSize) }.buttonStyle(PageButtonStyle()).cornerRadius(15)

Expanding & Collapsing

How is this going to work? Let’s think it through.

If we’re on a screen, the menu widget should be one button with the icon matching the current screen. When we tap it, it should expand and show all the buttons in the menu, and the button that did have the current screen’s icon should change to be an arrow of sorts pointing up, showing that the menu expanded. When a button in the menu is tapped, the screen switches. The menu should only collapse when the arrow button is tapped, and the arrow button should revert back to showing the current screen’s icon.

Let’s implement!

Under the existing variables, add expand (indicates if menu should be expanded) and icon (indicates the current screen’s icon).

struct ViewController: View {
@State var page:String = "ViewA"
let screenSize = UIScreen.main.bounds
let iconSize = UIScreen.main.bounds.width*0.07
@State var expand:Bool = false
@State var icon:String = "a.circle.fill"

Within the button VStack, add an if statement around all the existing buttons; this will show all buttons when expand is true, and only show the current screen’s button when false. Below the if statement, add another button; this will be the current screen/arrow button and should always show. Within each button action, add a variable assignment to icon that corresponds with the page; this is used for the current screen button.

VStack { 
if expand {
Button(action: {
self.page = "ViewA"
self.icon = "a.circle.fill"
}) {
Image(systemName: "a.circle.fill").resizable().frame(width: iconSize, height: iconSize)
}.buttonStyle(PageButtonStyle()).cornerRadius(15)
Button(action: {
self.page = "ViewB"
self.icon = "b.circle.fill"
}) {
Image(systemName: "b.circle.fill").resizable().frame(width: iconSize, height: iconSize)
}.buttonStyle(PageButtonStyle()).cornerRadius(15)
Button(action: {
self.page = "ViewC"
self.icon = "c.circle.fill"
}) {
Image(systemName: "c.circle.fill").resizable().frame(width: iconSize, height: iconSize)
}.buttonStyle(PageButtonStyle()).cornerRadius(15)
} //end of if statement

//Arrow/Current Screen
Button(action: {
self.expand.toggle()
}) {
Image(systemName: expand ? "chevron.up" : icon).resizable().frame(width: iconSize, height: expand ? iconSize/3 : iconSize)
}.buttonStyle(PageButtonStyle()).cornerRadius(15)

}.padding([.all]) //end of button vstack

On the “Arrow/Current Screen” button, we added an action that toggles expand, indicating if the menu should be expanded/collapsed. For the icon, we show the chevron arrow if the menu is expanded, and icon (whatever that was set to) if it isn’t (using a handy ternary operator). Additionally, we want the chevron arrow to be proportional, so if that’s the button icon (which it will be when expand is true), then shrink the height (accomplished using a ternary operator… they are the best!).

Better than Before

And here we go! Better button design, appropriate positioning on the screen, and an expanding/collapsing menu. Just about done.

Switch between screens, good button design, good menu positioning, menu expands and collapses.

Plain Jane gets an Upgrade

That looks nice, but is still kind of bleh. It’d be sooo much better with some animation and gestures, don’t you think?

Animation

This is incredibly easy, you won’t even believe it.

I want the chevron arrow to shrink and stretch when it’s clicked, and the menu to fade in and out when it expands and collapses. Just append .animation(.spring()) to the chevron button and the button VStack.

...
//Chevron/Current Screen
Button(action: {
self.expand.toggle()
}) {
Image(systemName: expand ? "chevron.up" : icon).resizable().frame(width: iconSize, height: expand ? iconSize/3 : iconSize)
}.buttonStyle(PageButtonStyle()).cornerRadius(15).animation(.spring())
}.padding([.all]).animation(.spring()) //end of button vstack

Gestures- Dragging the Menu

I realized after implementing this that sometimes the menu being on the right side blocks things on the screen behind it, and it’d be nice to be able to move it. And what if someone holds their phone in their left hand? It’s rather obnoxious to have to reach across the screen with your thumb to reach it.

So let’s implement a drag gesture. Under the existing variables, add rightSide (indicates whether the menu is on the right or left side).

struct ViewController: View {
@State var page:String = "ViewA"
let screenSize = UIScreen.main.bounds
let iconSize = UIScreen.main.bounds.width*0.07
@State var expand:Bool = false
@State var icon:String = "a.circle.fill"
@State private var rightSide:Bool = true

Below these variables and above the View body, add the menuDrag gesture. This will toggle rightSide if the user drags from the right to left half of the screen, and vice versa.

var menuDrag: some Gesture {
DragGesture().onChanged { value in }.onEnded { value in
if (self.rightSide && value.translation.width < -(self.screenSize.width*0.5)) || (!self.rightSide && value.translation.width > self.screenSize.width*0.5) {
self.rightSide.toggle()
}
}
}

We just appended .animation(.spring()) to the chevron button and the button VStack; let’s add .gesture(menuDrag) to those as well. We’ll also want to add a ternary operator to the alignment of the button ZStack.

...
//Chevron/Current Screen
Button(action: {
self.expand.toggle()
}) {
Image(systemName: expand ? "chevron.up" : icon).resizable().frame(width: iconSize, height: expand ? iconSize/3 : iconSize)
}.buttonStyle(PageButtonStyle()).cornerRadius(15).animation(.spring()).gesture(menuDrag)
}.padding([.all]).animation(.spring()).gesture(menuDrag) //end of button vstack
}.frame(width: screenSize.width, height: screenSize.height, alignment: rightSide ? .bottomTrailing : .bottomLeading) //end of button zstack

Voila!

Success!

And there’s more!

Now we have a custom menu widget that can be easily integrated into any SwiftUI project. It’s also incredibly easy to swap in your own colors (gradients even!), icons, screens, etc. You can even change it to expand horizontally or close when you tap any of the buttons, not just the chevron arrow.

Check out the project on GitHub for more.

Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

Sign up for Best Stories

By Dev Genius

The best stories sent monthly to your email. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Madison Gipson

Written by

Student & intern learning how to make the world a better place with code.

Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

Madison Gipson

Written by

Student & intern learning how to make the world a better place with code.

Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store