It’s all about order!

Creating Navigation Drawer with SwiftUI

Sidharth J Dev
Level Up Coding
Published in
6 min readApr 24, 2020

--

Most iOS developers out there would absolutely dread it when the design demands of a Navigation Drawer in their iOS app. Simply put, there is no direct way to implement it from iOS. I am not a fan of this design, because it is so un-iOS like. Having said that, when the need arises, we must put aside our differences and get the job done.

Until now, I have been dependent on libraries like SWRevealViewController for this. I tried subclassing Segues to make this work without being dependant on CocoaPods too much, but it was too hard. That changed recently, with me starting to learn SwiftUI.

So here is my code, implementing a Custom Navigation Drawer, using SwiftUI -

Let’s start with creating a Model for Menu Items

class MenuContent: Identifiable, ObservableObject {    var id = UUID()
var name: String = ""
var image: String = ""
init(name: String, image: String) {
self.name = name
self.image = image
}
}

This is a pretty straightforward definition of a Model. We give the menu a Title(name)and an Icon(image).

We conform our Class MenuContent to Identifiable and ObservableObject so that the list created from this class remains distinct(unique elements with Identifier represented by UUID) and we can observe changes made on the class object, respectively.

Next we can create a list of menuContents as a testData.

let menuHome = MenuContent(name: "Home", image: "house.fill")let menuProfile = MenuContent(name: "Profile", image: "person.fill")let menuChat = MenuContent(name: "Chat", image: "message.fill")let menuLogout = MenuContent(name: "Logout", image: "power")let menuContents = [menuHome, menuProfile, menuChat, menuLogout]

In WWDC 2019, Apple introduced SF Symbols and it has been gorgeous. A library of Vector images that we can use in our apps. Since they are Vector images, they are scalable and easily customisable. The image variable is accepting String names that match the title of Images available in SF Symbols.

For more information on SF Symbols, check out the official documentation below-

Now, let’s design our Home Screen-

struct ContentView: View {
var menu: [MenuContent] = menuContents
var body: some View {
List(menu) { menuItem in
MenuCell(menuItem: menuItem)
.onTapGesture {}
}
}
}
struct MenuCell: View { var menuItem: MenuContent = menuContents[0]
var body: some View {
HStack {
Image(systemName: menuItem.image)
.foregroundColor(
.orange)
Text(menuItem.name)
.foregroundColor(
.orange)
}
}
}

HomeScreen (ContentView) is now rendering a single List on the array menu . We have also created a separate View to render MenuCell(old habits from UITableView).

This code will give us the following output

Menu

Now let’s add a Target View for the menu item-

struct HomeView: View {
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(
.all)
VStack(alignment: .leading) {
Button(action: {}) {
Image(systemName: "line.horizontal.3")
.foregroundColor(.white)
}
.frame(width: 50.0, height: 50.0)
Spacer()
Text("This Is Home")
.font(
.largeTitle)
.foregroundColor(
.white)
.frame(maxWidth: .infinity)
Spacer()
}.padding(.horizontal) .frame(maxWidth: .infinity)
}
}
}

We can add this View to our ContentView on top of its existing Menu List as the next element on an ZStack

struct ContentView: View {
var menu: [MenuContent] = menuContents
var body: some View {
ZStack {
List(menu) { menuItem in
MenuCell(menuItem: menuItem).onTapGesture {
self.menuItemSelected = menuItem
}
}
HomeView()
}
}
}

After adding HomeView() we won’t be able to see our existing Menu List, because the HomeView draws itself on top of the Menu.

What might happen if we add an offset property to our HomeView ?

struct HomeView: View {
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(
.all)
VStack(alignment: .leading) {
Button(action: {}) {
Image(systemName: "line.horizontal.3")
.foregroundColor(.white)
}
.frame(width: 50.0, height: 50.0)
Spacer()
Text("This Is Home")
.font(
.largeTitle)
.foregroundColor(
.white)
.frame(maxWidth: .infinity)
Spacer()
}.padding(.horizontal) .frame(maxWidth: .infinity)
}
.offset(x: 200.0, y: 0)
}
}
HomeView being rendered from ContentView

Isn’t this interesting? Now, the View rendered on the ZStack is pushed away. This gives it the appearance of a Navigation Drawer. Let’s now add an @State var property inside the HomeView on which we can dynamically switch the MenuShowing Property-

struct HomeView: View {
@State private var showingMenu = false
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(
.all)
VStack(alignment: .leading) {
Button(action: {self.showingMenu.toggle()}) {
Image(systemName: "line.horizontal.3")
.foregroundColor(.white)
}
.frame(width: 50.0, height: 50.0)
Spacer()
Text("This Is Home")
.font(
.largeTitle)
.foregroundColor(
.white)
.frame(maxWidth: .infinity)
Spacer()
}.padding(.horizontal) .frame(maxWidth: .infinity)
}
.offset(x: showingMenu ? 200.0 : 0.0, y: 0)
}
}

showingMenu is a Boolean on which, the offset is depending upon. As you can see, when the user clicks the Menu Button, this value is toggled and it can present and hide the menu.

I’m just gonna go ahead and add a small snippet of code to give an animation to this transition-

.animation(.easeOut)

Let’s just add the rest of the Views to connect the Menus to.

struct HomeView: View {
@State private var showingMenu = true
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(
.all)
VStack(alignment: .leading) {
Button(action: {self.showingMenu.toggle()}) {
Image(systemName: "line.horizontal.3")
.foregroundColor(.white)
}
.frame(width: 50.0, height: 50.0)
Spacer()
Text("This Is Home")
.font(
.largeTitle)
.foregroundColor(
.white)
.frame(maxWidth: .infinity)
Spacer()
}.padding(.horizontal) .frame(maxWidth: .infinity)
}
.offset(x: showingMenu ? 200.0 : 0.0, y: 0)
.animation(.easeOut)
}
}
struct ProfileView: View {
@State private var showingMenu = false
var body: some View {
ZStack {

Color.red.edgesIgnoringSafeArea(
.all)
VStack(alignment: .leading) {
Button(action: {self.showingMenu.toggle()}) {
Image(systemName: "line.horizontal.3")
.foregroundColor(.white)
}
.frame(width: 50.0, height: 50.0)
Spacer()
Text("This Is Profile")
.font(
.largeTitle)
.foregroundColor(
.white)
.frame(maxWidth: .infinity)
Spacer()
}.padding(.horizontal) .frame(maxWidth: .infinity)
}
.offset(x: showingMenu ? 200.0 : 0.0, y: 0)
.animation(.easeOut)
}
}
struct ChatView: View {
@State private var showingMenu = false
var body: some View {
ZStack {

Color.blue.edgesIgnoringSafeArea(
.all)
VStack(alignment: .leading) {
Button(action: {self.showingMenu.toggle()}) {
Image(systemName: "line.horizontal.3")
.foregroundColor(.white)
}
.frame(width: 50.0, height: 50.0)
Spacer()
Text("This Is Chat")
.font(
.largeTitle)
.foregroundColor(
.white)
.frame(maxWidth: .infinity)
Spacer()
}.padding(.horizontal) .frame(maxWidth: .infinity)
}
.offset(x: showingMenu ? 200.0 : 0.0, y: 0)
.animation(.easeOut)
}
}
struct LogoutView: View {
@State private var showingMenu = false
var body: some View {
ZStack {

Color.green.edgesIgnoringSafeArea(
.all)
VStack(alignment: .leading) {
Button(action: {self.showingMenu.toggle()}) {
Image(systemName: "line.horizontal.3")
.foregroundColor(.white)
}
.frame(width: 50.0, height: 50.0)
Spacer()
Text("This Is Logout")
.font(
.largeTitle)
.foregroundColor(
.white)
.frame(maxWidth: .infinity)
Spacer()
}.padding(.horizontal) .frame(maxWidth: .infinity)
}
.offset(x: showingMenu ? 200.0 : 0.0, y: 0)
.animation(.easeOut)
}
}

In the ContentView we shall create a function to return a View needed based on Menu item selected.

func selected(Menu: MenuContent) -> some View {
switch Menu.name {
case "Home":
return AnyView(HomeView())

case "Profile":
return AnyView(ProfileView())

case "Chat":
return AnyView(ChatView())

case "Logout":
return AnyView(LogoutView())
default:
return AnyView(HomeView())
}
}

Finally, we should modify the Body of the ContentView to change screens on click of MenuCell.

struct ContentView: View {
var menu: [MenuContent] = menuContents
@State var menuItemSelected: MenuContent = menuContents[0]
var body: some View {
ZStack {
List(menu) { menuItem in
MenuCell(menuItem: menuItem).onTapGesture {
self.menuItemSelected = menuItem
}
}
self.selected(Menu: menuItemSelected)
}
}
}

With menuItemSelected being an @State var , everytime value in it changes, the ContentView will re-render itself and change the top view of the ZStack .

Final output will look something like this-

Final Screen

This is one of the many ways that one can implement a Navigation Drawer in iOS, using SwiftUI. It has never been easier.

If anybody is interested, they can check out the full code at-

Cheers!

--

--