Creating Navigation Drawer with SwiftUI
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
toIdentifiable
andObservableObject
so that the list created from this class remains distinct(unique elements with Identifier represented byUUID
) 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
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)
}
}
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-
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!