Custom Navigation in SwiftUI

The information in this tutorial is out of date, please click here for a completely rehauled and updated version.

Finished Product

Standard Navigation Controllers given within swiftUI's framework are great and allow for quite a bit of flexibility. Sometimes though, they are missing a key piece of functionality you are looking for, or maybe you don't like the standard transition included in them.

Luckily, implementing your own custom solution to navigation is easy, and does not require much more work compared to a standard navigation controller once set up.

First:

Set up a custom struct which will be your actual navigation bar

struct NavController: View {

    // set the title of the navigation bar, allow it to be dynamic
    @Binding var title: String

    // pass button(s) you want the nav bar to have
    var trailing_Buttons: AnyView
    var leading_Button: AnyView

    // set some constants true for all nav bars
    let textColor = Color.white
    let navBarColor = Color(.sRGB, red: 225/255, green: 75/255, blue: 75/255, opacity: 1)

    // the nav bar itself
    var body: some View {
        HStack(spacing: 15) {
            // the buttons that are shown before the title
            Group {
                self.leading_Button
            }
            // the nav title itself
            Text(self.title)
                .font(.title)
                .fontWeight(.bold)
                .foregroundColor(self.textColor)
                .lineLimit(1)

            // push the other buttons as far as needed
            Spacer(minLength: 0)
            // buttons shown after the title
            Group {
                self.trailing_Buttons
            }
        }
        // give the entire view some padding
        .padding()
        // set the background color of the bar
        .background(self.navBarColor.edgesIgnoringSafeArea(.all))
    }
}

This Struct has fields for the button shown before the title, the title itself, and the buttons shown after the title. This allows for powerful flexibility when implementing in other views.

Next:

In the view you are planning on using the NavController, specify a State variable for the title, and two functions that will return the buttons you are planning on using for the nav controller.

// set an updateable variable for the title
    @State var title: String = "Navigation"

func trailing_Buttons() -> AnyView {
        return AnyView(
            // hstack for when you want more than one
            HStack(spacing: 10) {
                Button(action: {
                    print("xmark")
                }, label: {
                    Image(systemName: "xmark")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 18)
                        .foregroundColor(Color.white)
                })
                Button(action: {
                    print("trash")
                }, label: {
                    Image(systemName: "trash")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 18)
                        .foregroundColor(Color.white)
                })
            }
        )
    }
    // function for returning what will be the leading button
    func leading_Button() -> AnyView {
        // return an AnyView
        return AnyView(
            // the button
            Button(action: {
                print("text.justifyleft")
            }, label: {
                // system image
                Image(systemName: "text.justifyleft")
                    .resizable()
                    .frame(width: 18, height: 18)
                    .foregroundColor(Color.white.opacity(0.75))
            })
        )
    }

Tip:

Wrapping your views with 'AnyView' when dealing with several different types of content in the same place. i.e Images, Buttons, Text, etc.

Lastly:

Implement the navigation controller in with your view in a VStack, passing in the variables you initialized!

var body: some View {
        // put into a vstack not contained in a scroll view
        VStack(spacing: 0) {
            // call the Nav Controller struct and pass the expected types
            NavController(title: self.$title, trailing_Buttons: self.trailing_Buttons(), leading_Button: self.leading_Button())
            // then you are free to put whatever here!
            Group {
                List {
                    Text("One 1")
                    Text("Two 2")
                    Text("Three 3")
                }
                .listStyle(GroupedListStyle())
                .environment(\.horizontalSizeClass, .regular)
            }
        }
    }

Tip:

If you implement an if/else statement inside the group I specified above, you can use the same NavController for different views. Also, I have provided a sample transition to attach as a modifier on the group. When changing the bool that controls which view is shown with an animation, this transition replicates the look of the default transition style.

@State var showNewView: Bool = false

.transition(.asymmetric(insertion: self.showNewView ? .move(edge: .trailing) : .move(edge: .leading), removal: self.showNewView ? .move(edge: .leading) : .move(edge: .trailing)))

That is it! you can reuse the NavController at the top of any other view you would like.

Leave a Reply

Your email address will not be published. Required fields are marked *