SwiftUI Menus
Using HStack To Create Menuing System
BY: David L. Collison
Background - Why Are We Doing This
SwiftUI doesn’t come with built in controls to handle menus. You must ‘roll your own’ with controls that are not dedicated to providing application menus.
This series of posts will take you thru building a menu system that is easy to read, easy to understand and gives you, the developer, significant control over how the menu will look.
So what do we want our menu system to look like. Well, for the purposes of this exercise, we want to use the Navigation Bar, and I want an Icon on the left side of the Navigation Bar that the user can tap to bring up the menu.
Note, that when the user selects the menu icon or button, the current screen will shrink in size, slide to the right and drop down from the top of the screen. You’ll also note that the current screen is deactivated when it is reduced and size and moved.
The left side of the screen is then populated with a menu that appears below the icon that was tapped. The menu consists of an image and text describing the menu option.
When the user taps the needed menu option, the menu will slide off the screen. The screen itself will change to the appropriate view and the view will once again take up the entire screen at the right size and be activated.
Assumptions - Things You Need To Know
- You know your way around Xcode. This isn’t a tutorial on how to use the development environment. If you’re new to Swift/SwiftUI, you’ll be better off heading to other sites/posts/videos to gain an understanding of how to program in Swift and build interfaces in SwiftUI.
- You understand SwiftUI and can build basic interfaces using the SwiftUI common controls. Again, this is not going to be a full blown tutorial on how to use SwiftUI, but on how to build on the knowledge you have of SwiftUI to build user menus.
- You understand how to test your SwfitUI Views using the xCode canvas tool. Canvas is a great feature that allows you to see how your SwiftUI Views will appear on various iPhone and iPad devices. It is by no means a replacement fo the Simulator or running your code on an actual device, but it saves a lot of time during the ‘hacking’ stage where you’re making lots of changes to the UI as it is being built. Canvas will save you tons of time by not having to wait to spin up the Simulator or to build and download the app to an actual iPhone or iPad device.
- You are familiar with the SF Symbols supplied by Apple. This is a set of artwork provided by Apple for use in your applications. They can be resized without degrading the overall image quality and are easy to use within your applications. If you’re not familiar with SF Symbols, quick hit Apples web site and learn about these common images that will help make your apps look more professional. Better yet, download the SF Symbols app from Apple to have the reference at your fingertips.
Setup - Yes, You Have Some Work To Do
- Because you know Xcode, create a new project called YourMenuSystem. It doesn’t need to use Core Data, so don’t check that box. It won’t hurt anything if you do, so your call.
- Grab yourself a cup of coffee, or whatever you use to keep the energy flowing.
- Bonus - throw on your favorite play list and enjoy the tunes.
Build Supporting Views - Boring But Necessary
Before we actually build the menu, we need something for the menu to do. The primary purpose of our menuing system is to switch views that the user will interact with to perform some function. Typical of many programs out in the App Store.
We’re going to build some quick stub screens. I’m going to walk you thru building some quick screens that will demonstrate the menu system and provide parent and child screens that will demonstrate moving around a typical application.
Our stub screens will consist of the following:
- Companies parent and child screens.
- Employees parent and child screens.
- Settings parent and child screens.
If you’re not familiar with the term parent and child screens, in this exercise the terms will be defined as follows:
- Each parent screen will contain a scrollable list of items that can be selected by the user.
- Each child screen will contain detail associated with the selected parent item that can be displayed to the user.
You can put all the following in one file or you can split into separate files for each unique View. For the purposes of this exercise, I personally threw them all together in one file. I know, I know, bad form on my part, but these simple views are not what I’m trying to show here - the menu is what’s important. Please don’t report me to the coding cops.
So, let’s get building - first up is the Companies Parent Screen. Create a new SwiftUI file called StubViews and place all of the below Views within this file:
import SwiftUI
struct CompaniesView: View {
var body: some View {
List {
ForEach((1...50), id: \.self) { contentCount in
NavigationLink(destination:
CompanyView(companyId: contentCount)) {
Text("Company \(contentCount)")
}
}
}
.listStyle(PlainListStyle())
.navigationBarTitle("Companies")
}
}
And now the Company Child Screen:
struct CompanyView: View {
var companyId: Int
var body: some View {
VStack {
Text("Company View")
Text("Id: \(companyId)")
}
.navigationBarTitle("Company")
}
}
You’ll note that we use the NavigationLink to transition the View on the screen to the Child Screen. In this instance, we pass in a numeric value identifying the particular Company that was selected. We’ll display that Company ID in the Child View to validate the Child View is displaying the correct information.
We’re essentially going to take the above code, copy it and make the slightest of modifications to reference both the Employees and Settings stub screens:
struct EmployeesView: View {
var body: some View {
List {
ForEach((1...50), id: \.self) { contentCount in
NavigationLink(destination:
EmployeeView(employeeId: contentCount)) {
Text("Employee \(contentCount)")
}
}
}
.listStyle(PlainListStyle())
.navigationBarTitle("Employees")
}
}
struct EmployeeView: View {
var employeeId: Int
var body: some View {
VStack {
Text("Employee View")
Text("Employee Id: \(employeeId)")
}
.navigationBarTitle("Employee")
}
}
struct SettingsView: View {
var body: some View {
List {
ForEach((1...50), id: \.self) { contentCount in
NavigationLink(destination:
SettingView(settingId: contentCount)) {
Text("Setting \(contentCount)")
}
}
}
.listStyle(PlainListStyle())
.navigationBarTitle("Settings")
}
}
struct SettingView: View {
var settingId: Int
var body: some View {
VStack {
Text("Setting View")
Text("Setting Id: \(settingId)")
}
.navigationBarTitle("Setting")
}
}
Hey, look at that! You’ve built a set of screens that do absolutely nothing. But, seriously, you’ve quickly created some stub screens that we can use to test the functionality of the menuing system. Bonus, it didn’t really take that long to do!
Helpers - Everyone Can User A Helper!
Creating the menu is not just SwiftUI code manipulating the UI. The menuing system needs some small chunks of code to make it operate properly. So we are going to work our way thru each of these helper pieces.
Identifying Menu Options - Enum Is Your Friend
We need a quick way to define menu options that will be available to the users and then to be able to cross check those when selected to ensure that we display the correct screen. Swifts enum (enumeration) construct gives us a win and allows us to do this in a pretty straight forward way.
My preference is to put this in its own file - create a file titled MenuItemsControl.swift and place the following code in the file:
enum MenuItems {
case Home
case Companies
case Employees
case Modifier
case Validator
case Settings
}
If you look back to the example menu screenshot that we had at the beginning of the post, you’ll see that there is a case statement representing each item in the menu. This enumeration structure will then allow us to ensure that we are accounting for all menu items when responding to the users selection.
Switching Views - Controlling What The User Sees
Next we need a way to change the display so that it responds to and shows the user whatever is associated with the menu selection that they just made. It also needs to have a default landing page that is shown when the user first opens the app.
This is a very simple piece of code containing a Swift Switch statement that will return the currently selected view.
Being as this works in tandem with the enum structure you just built, I’m recommending that you place it in the same file below the enum construct. This allows the menu definition and control to be in one file. So create the following code below the enum structure:
import SwiftUI
struct PageView: View {
var menuSelection: MenuItems = MenuItems.Companies
var body: some View {
switch menuSelection {
case MenuItems.Employees:
EmployeesView()
case MenuItems.Settings:
SettingsView()
case MenuItems.Modifier:
EmptyView()
case MenuItems.Validator:
EmptyView()
default:
CompaniesView()
}
}
}
Some important pieces of code that you need to understand before we go any further. You did say you wanted to learn, right?
var menuSelection: MenuItems = MenuItems.Companies
The above line of code acts as a parameter in the View structure. Allowing you to pass in the selected menu item so that the system can return the proper view to the display. If you choose not to pass in the user selected option, the system will default to displaying the Companies View.
EmptyView()
I’m highlighting the above as a way to build out your menu without actually having the associated views actually built. If you haven’t built the view, you can simply return an Empty View to the display. This allows you to build the menu structure first, then build all of the views that the menu will end up controlling and finally step back into the menu code and call each individual view as it is completed. Good stuff!
Finally, you have the following two lines of code:
default:
CompaniesView()
Note, there is not a Companies switch statement to match up to the Companies enum structure. Why? You can, my preference is to create a default that the menu control will use if you forget to create an item that matches the full enum structure. Just a preference on my part, but it will help you build out the actual code controlling the menu without having to have all the options and screens built upfront.
Modifiers - They Make You Look Good
The last couple of chunks of code that I want to talk about (related to helpers), is the modifiers. SwiftUI lets you create little slices of code that you can use over an over that control the look and feel of specific visual elements on the screen.
I’ve got two little pieces of code that help define how the overall menu will look and how the actual pages being viewed by the user will look. I place each of these in their own file, and I would recommend that you do the same.
Create a file titled ModMenuItemStack.swift and include the following code:
import SwiftUI
struct ModMenuItemStack: ViewModifier {
func body(content: Content) -> some View {
content
.background(Color(.secondarySystemBackground))
.offset(x: 0, y: 0)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0,
maxHeight: .infinity, alignment: .topLeading)
}
}
The above code is pretty straight forward. It takes whatever SwiftUI element you have it associated with ‘content’ and applies the following:
- Sets the background color to the Secondary System Background color.
- Ensures that the element is pinned to the top, left corner of its parent window.
- Ensures that the menu fills the width that it is assigned to.
- Ensures that the menu is aligned to the Top of the space it is assigned.
Next up is the modifier that we will use to control the information being displayed to the user. Create a file titled ModPageStack.swift and insert the following code:
import SwiftUI
struct ModPageStack: ViewModifier {
var isMenuVisible: Bool = false
func body(content: Content) -> some View {
content
.padding(0)
.offset(x: 0, y: self.isMenuVisible ? 50 : 0)
.disabled(self.isMenuVisible)
.scaleEffect(self.isMenuVisible ? 0.75 : 1)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0,
maxHeight: .infinity, alignment: .topLeading)
}
}
Again, this stuff is pretty straight forward. It takes whatever SwiftUI element you have it associated with ‘content’ and applies the following:
- It removes any padding that the system may try to apply around the user interface elements contained within the content.
- If adjusts where the content is displayed based on whether the menu is shown.
- If the menu is not shown the content will appear in the top, left corner.
- If the menu is displayed it will shift over to the right and will shift down from the top of the screen.
- If the menu is displayed, it will disable the content controlled within the user interface elements contained within the content.
- If the menu is displayed, it will scale the contents to be 75% of the regular size.
- it sets the frame of where the content is to take the maximum space allowed and aligns it to the top of the space.
Menu Item Builder - Performing The Magic
One last thing, the actual menu item needs a little bit of code to make it look right. Yes, I could write all this stuff over and over for each menu item, but that feels like a waste of time.
So, create a new file labeled MenuItemAlt.swift and place the following code in the file:
import SwiftUI
struct MenuItem: View {
var symbolName: String
var itemName: String
var action: () -> Void
var body: some View {
HStack {
Image(systemName: symbolName)
Text(self.itemName)
Spacer()
}
.font(.title2)
.padding(5)
.padding(.top, 15)
.onTapGesture {
action()
}
}
}
This short, but its a little more intricate, so we’ll spend a few moments walking thru what all is happening in this small file.
There are three variables that you must provide if this is going to work:
- The SF Symbol (see Apple documentation) that will be a visual identifier of what the menu item represents. The SF Symbol image will be displayed ahead of the actual menu item text.
- The menu item name, this is the actual description you want displayed on the menu that represents the screen that will be displayed when the user selects the item.
- The action you want to take once the user actually selects this item. This is a short piece of code that typically tells the menu to disappear back into the void and to set the option for which display to present to the user.
- Create a HStack that contains the menu image and menu text.
- Format the menu item:
- So that it is sized properly: font(.title2)
- That there is a small empty space around the entire menu item: padding(5)
- That there is a larger space between individual menu items: padding(.top, 15)
- That the menu item recognizes when the user has selected it by pressing on it and executes the action: onTapGesture { action () }
HStack Menu - The Real Story
Ok, let’s get to it! I’ve shown you what I want the menu to look like and we’ve walked thru all the helpers that will get us there. Now we need to discuss the main event - the magic of our menu and how it is controlling the display on your device to actually show the user what they want to see.
There are two primary components with the HStack that we will be discussing. The first element is a VStack that will actually contain each of the menu elements. The second is the group that will contain the actual screen associated with the selected menu item. It is that simple!
Create a file titled HomeView.swift and place the following code within the file:
import SwiftUI
struct HomeView: View {
@State private var isMenuVisible: Bool = false
@State private var menuSelection: MenuItems = MenuItems.Home
var body: some View {
ZStack {
NavigationView {
HStack {
// Display the menu option when visible.
if (self.isMenuVisible) {
VStack {
MenuItem(symbolName: "house.circle.fill",
itemName: "Companies", action:
{
self.menuSelection = MenuItems.Companies
self.isMenuVisible = false
})
MenuItem(symbolName: "person.crop.circle.fill",
itemName: "Employees", action:
{
self.menuSelection = MenuItems.Employees
self.isMenuVisible = false
})
MenuItem(symbolName: "link.circle.fill",
itemName: "Modifier", action:
{
self.menuSelection = MenuItems.Modifier
self.isMenuVisible = false
})
MenuItem(symbolName: "checkmark.circle.fill",
itemName: "Validator", action:
{
self.menuSelection = MenuItems.Validator
self.isMenuVisible = false
})
MenuItem(symbolName: "gearshape.fill",
itemName: "Settings", action: {
self.menuSelection = MenuItems.Settings
self.isMenuVisible = false
})
}.modifier(ModMenuItemStack())
} else {
EmptyView()
}
// Display the proper screen based on menu selection.
Group {
PageView(menuSelection: self.menuSelection)
}.modifier(ModPageStack(isMenuVisible: self.isMenuVisible))
}
.navigationBarTitle("G9Concepts")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading, content: {
Button(action: {
self.isMenuVisible.toggle()
}) {
Image(systemName: "filemenu.and.selection")
}
})
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
And, we’re done! It was that simple! Ok, yes, I’ve got some ‘splainin to do. Let’s break it down.
There are variables that the menu system will need to monitor to understand whether the menu should be displayed and what menu option is currently selected and should be displayed. The variables are declared with the State and Private identifiers. State is used to inform the SwiftUI system to monitor the variable in realtime and alert the View if the value fo the variable changes. The private declaration ensures that code outside of the menu can’t mess around and change it without your knowledge.
@State private var isMenuVisible: Bool = false
@State private var menuSelection: MenuItems = MenuItems.Home
We’re interested in the contents within the HStack. If you need to understand ZStack or NavigationView, please look at the dozens of tutorials or posts on the web.
Remember, HStack allows you to display items side by side on the screen. This is exactly what we want when the menu is being displayed. However, we don’t want it to appear once the user has made a choice, so we need a way to have it shown and then to disappear. This is the magic sauce:
if (self.isMenuVisible) {
…
} else {
EmptyView()
}
We use a simple if statement to control whether the menu is displayed within the HStack. If the menu is not supposed to be shown, then the system will display an Empty View - quite literally, nothing.
If the menu is supposed to be shown on the screen, the system will build the menu within a VStack and each menu item will be constructed with code similar to the following:
MenuItem (symbolName: "house.circle.fill",
itemName: "Companies",
action: {
self.menuSelection = MenuItems.Companies
self.isMenuVisible = false
})
Elegant, isn’t it? This aligns with the MenuItem helper that we built just a few minutes ago and we pass in the three critical elements:
- Symbol Name - representing the SF Symbol description to display the image element representing the menu item.
- Item Name - representing the text that will be displayed on the menu.
- Action - a little snippet of code that will store the identity of the screen that the user wants displayed. And then flips the indicator to hide the menu.
Put the exclamation point on it, drop the mic, you’re done!
You’ve quickly pulled together a menu system that you can use in any of your applications. It’s clean, it’s quick and it works. I
f it helps you fantastic, if you don’t like it and want me to shred it, well it’s the internet, hit the next button and you never have to see it again.
Drop me a note if you like, dislike or have general comments on the code.
If you'd like more information on my background: LinkedIn Profile
#Swift #SwiftUI #HStack #Menu #DavidLCollison #Programming #SoftwareDevelopment