SwiftUI · · 4 min read

How to Create an Animated Navigation Menu in SwiftUI Using matchedGeometryEffect

How to Create an Animated Navigation Menu in SwiftUI Using matchedGeometryEffect

One reason why I really enjoy programming using the SwiftUI framework is that it makes so easy to animate view changes. In particular, the introduction of the matchedGeometryEffect modifier, introduced in iOS 14, further simplifies the implementation of view animations. With matchedGeometryEffect, all you need is describe the appearance of two views. The modifier will then compute the difference between those two views and automatically animates the size/position changes.

We have written a detailed tutorial on matchedGeometryEffect. I highly recommend you to check it out if this is the very first time you come across this modifier. In this tutorial, we will make use of matchedGeometryEffect to develop an animated navigation menu like the one shown below.

swiftui-animated-navigation-menu-matchedGeometryEffect

Editor’s Note: To dive deeper into SwiftUI animation and learn more about the SwiftUI framework, you can check out the book here.

Creating the Navigation Menu

Before we create the animated menu, let’s start by creating the static version. As an example, the navigation menu only displays three menu items.

navigation-menu-swiftui

To layout three text views horizontally with equal spacing, we use the HStack view and Spacer to arrange the views. Here is the code sample:

struct NavigationMenu: View {

    let menuItems = [ "Travel", "Nature", "Architecture" ]

    var body: some View {
        HStack {
            Spacer()

            Text(menuItems[0])
                .padding(.horizontal)
                .padding(.vertical, 4)
                .background(Capsule().foregroundColor(Color.purple))
                .foregroundColor(.white)

            Spacer()

            Text(menuItems[1])
                .padding(.horizontal)
                .padding(.vertical, 4)
                .background(Capsule().foregroundColor(Color(uiColor: .systemGray5)))

            Spacer()

            Text(menuItems[2])
                .padding(.horizontal)
                .padding(.vertical, 4)
                .background(Capsule().foregroundColor(Color(uiColor: .systemGray5)))

            Spacer()
        }
        .frame(minWidth: 0, maxWidth: .infinity)
        .padding()
    }
}

As you can see from the code above, it contains quite a lot of duplications. It can be further simplified with ForEach:

struct NavigationMenu: View {
    @State var selectedIndex = 0
    var menuItems = [ "Travel", "Nature", "Architecture" ]

    var body: some View {
        HStack {
            Spacer()

            ForEach(menuItems.indices) { index in

                if index == selectedIndex {
                    Text(menuItems[index])
                        .padding(.horizontal)
                        .padding(.vertical, 4)
                        .background(Capsule().foregroundColor(Color.purple))
                        .foregroundColor(.white)
                } else {
                    Text(menuItems[index])
                        .padding(.horizontal)
                        .padding(.vertical, 4)
                        .background(Capsule().foregroundColor(Color(uiColor: .systemGray5)))
                        .onTapGesture {
                            selectedIndex = index
                        }
                }

                Spacer()
            }

        }
        .frame(minWidth: 0, maxWidth: .infinity)
        .padding()
    }
}

We added a state variable named selectedIndex to keep track of the selected menu item. When the menu item is selected, we highlight it in purple. Otherwise, its background color is set to light gray.

To detect users’ touch, we attached the .onTapGesture modifier to the text view. When it’s tapped, we update the value of selectedIndex to highlight the selected text view.

Animating the Navigation Menu

Now that we’ve implemented the navigation menu, however, it misses the required animation. To animating the view change whenever a menu item is selected, all we need to do is create a namespace variable and attach the matchedGeometryEffect modifier to the text view in purple:

struct NavigationMenu: View {
    @Namespace private var menuItemTransition

    .
    .
    .

    var body: some View {
        HStack {
            Spacer()

            ForEach(menuItems.indices) { index in

                if index == selectedIndex {
                    Text(menuItems[index])
                        .padding(.horizontal)
                        .padding(.vertical, 4)
                        .background(Capsule().foregroundColor(Color.purple))
                        .foregroundColor(.white)
                        .matchedGeometryEffect(id: "menuItem", in: menuItemTransition)
                } else {
                    .
                    .
                    .
                }

                Spacer()
            }

        }
        .frame(minWidth: 0, maxWidth: .infinity)
        .padding()
        .animation(.easeInOut, value: selectedIndex)
    }
}

The ID and namespace are used for identifying which views are part of the same transition. We also need to attach the .animation modifier to the HStack view to enable the view animation. Note that this project is built using Xcode 13. The animation modifier is updated in the new version of iOS. You have to provide the value to monitor for changes. Here, it’s the selectedIndex.

Once you made the changes, you can test the NavigationMenu view in a simulator. Tap a menu item and you will see a nice animation when the item is transited from one state to another.

animated-swiftui-navigation-menu

Using the Animated Navigation Menu View

To apply this animated navigation menu to your project, you can modify the NavigationMenu view to accept a binding to the selected index:

@Binding var selectedIndex: Int

For example, you have created a page-based tab view like this:

struct ContentView: View {
    @State var selectedTabIndex = 0
    let menuItems = [ "Travel", "Film", "Food & Drink" ]

    var body: some View {
        TabView(selection: $selectedTabIndex) {

            ForEach(menuItems.indices) { index in
                Text(menuItems[index])
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                    .background(Color.green)
                    .foregroundColor(.white)
                    .font(.system(size: 50, weight: .heavy, design: .rounded))
                    .tag(index)
            }
        }
        .tabViewStyle(.page(indexDisplayMode: .never))
        .ignoresSafeArea()
        .overlay(alignment: .bottom) {
            NavigationMenu(selectedIndex: $selectedTabIndex, menuItems: menuItems)
        }
    }
}

You can add the NavigationMenu view as an overlay and use your own menu items.

swiftui-tab-view-animated-tab-bar

Read next