My name is AJ Picard, and I’m an app designer here at MartianCraft. Even though I love designing and pushing pixels every day, one thing I’m trying to get better at is coding … with SwiftUI in particular.

SwiftUI is a coding framework Apple launched a couple of years ago. I’m not a developer by any means, but I have found that it is easy to design and create animations using SwiftUI. In this tutorial, we’re going to create a floating menu button that animates when tapped.

Let’s start by showing what the end result looks like for the components we’re building in this tutorial:

Thinking through the problem of creating this, we can come up with the following goals:

  1. Add an element for each component, including: a. The plus button used to toggle the menu’s display b. The three buttons that appear/disappear
  2. Layer (or stack) these elements so that the three appearing/disappearing buttons hide behind the plus button.
  3. When the plus button is tapped, animate the display/removal of the three underlying buttons.

Xcode showing the code below in the SwiftUI Editor

The first bit of code accomplishes goals 1 and 2:

struct ContentView: View {
	var body: some View {
		ZStack {
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "pencil.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "message.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "person.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			ZStack {
				Circle()
					.frame(width: 44, height: 44)
					.foregroundColor(.blue)
				
				Image(systemName: "plus.circle")
					.foregroundColor(.white)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			
		}
	}
}

We’re starting with a ZStack with four nested ZStacks. Each child ZStack contains a circle and an SF Symbol icon. The SF Symbol icon library from Apple is not essential for app development, but if you’d like to browse the catalog you can download the app here.

In the preview, you should only see one blue circle with a plus icon. Why can’t you see all four icons? ZStacks stack elements on top of each other, so the children of the parent ZStack are one on top of another. Because the plus icon is last in the list, it was added last and is visible on top.

To accomplish the third goal, we need to add an onTapGesture to the plus icon to animate the display and removal of the content underneath it.

Xcode showing the code below in the SwiftUI Editor

Add a boolean @State variable named isMenuOpen and an onTapGesture modifier to the ZStack that contains plus.circle. This variable will control the animation when the plus icon is tapped.

struct ContentView: View {
	@State var isMenuOpen = false
	var body: some View {
		ZStack {
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "pencil.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "message.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "person.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			ZStack {
				Circle()
					.frame(width: 44, height: 44)
					.foregroundColor(.blue)
				
				Image(systemName: "plus.circle")
					.foregroundColor(.white)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			.onTapGesture {
				isMenuOpen.toggle()
			}
		}
	}
}

Here, we’ve added our isMenuOpen state variable at the top of the file, and we set the value equal to false.

Along with adding the state to the file, we added an onTapGesture to the ZStack that contains the plus icon, and we added isMenuOpen.toggle to the onTapGesture so that when the plus icon is tapped our isMenuOpen state will toggle from false to true.

The last step is to tell the elements stacked under the plus icon to do something when isMenuOpen is set to true.

struct ContentView: View {
	
	@State var isMenuOpen = false
	
	var body: some View {
		ZStack {
			
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "pencil.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			.shadow(color: .black.opacity(isMenuOpen ? 0.1 : 0.0), radius: 10, x: 0, y: 5)
			.offset(y: isMenuOpen ? -150 : 0)
			.opacity(isMenuOpen ? 100 : 0)
			.animation(.spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.5), value: isMenuOpen)
			
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "message.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			.shadow(color: .black.opacity(isMenuOpen ? 0.1 : 0.0), radius: 10, x: 0, y: 5)
			.offset(y: isMenuOpen ? -100 : 0)
			.opacity(isMenuOpen ? 100 : 0)
			.animation(.spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.5), value: isMenuOpen)
			
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "person.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
				
			}
			.shadow(color: .black.opacity(isMenuOpen ? 0.1 : 0.0), radius: 10, x: 0, y: 5)
			.offset(y: isMenuOpen ? -50 : 0)
			.opacity(isMenuOpen ? 100 : 0)
			.animation(.spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.5), value: isMenuOpen)
			
			ZStack {
				Circle()
					.frame(width: 44, height: 44)
					.foregroundColor(.blue)
				
				
				Image(systemName: isMenuOpen ? "x.circle" : "plus.circle")
					.foregroundColor(.white)
					.font(.system(size: 22, weight: .bold, design: .rounded))
					.animation(.easeInOut(duration: 0.3), value: isMenuOpen)
			}
			
			.onTapGesture {
				isMenuOpen.toggle()
			}
		}
		
	}
}

Now, there’s a lot going on in this step, so let’s break down what we’ve added. What’s great about SwiftUI is that it makes it very easy to animate things.

In the code above you see a lot of lines that look like this: isMenuOpen ? (value) : (value). The ? means “when isMenuOpen is set to true, do this,” and the : means “when isMenuOpen is set to false, do this.” This is known as the ternary operator.

Using that ternary operator, we’ve added multiple conditions for .shadow, .offset, and .opacity to each ZStack under the one holding the plus icon. This allows us to create the animation we want when we tap the plus icon: The .offset condition moves the element, the .opacity condition changes its opacity from 0 to 100, and the .shadow condition adds a box shadow to the element.

Finally, we have to add .animation to everything we’re animating so that the transition is smooth. You can play with the animation values to create different types of animation.

The end result should look like the preview video above.

You may have noticed that a lot of lines in this code look quite similar. To wrap this animation up into a more readable and maintainable bit of code, you can create a custom ViewModifier, as shown in the final implementation below:

struct ContentView: View {
	
	@State var isMenuOpen = false
	
	var body: some View {
		ZStack {
			
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "pencil.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			.displayOnMenuOpen(isMenuOpen, offset: -150)
			
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "message.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
			}
			.displayOnMenuOpen(isMenuOpen, offset: -100)
			
			ZStack {
				Circle()
					.frame(width: 38, height: 38)
					.foregroundColor(.white)
				
				Image(systemName: "person.circle")
					.foregroundColor(.blue)
					.font(.system(size: 22, weight: .bold, design: .rounded))
				
			}
			.displayOnMenuOpen(isMenuOpen, offset: -50)
			
			ZStack {
				Circle()
					.frame(width: 44, height: 44)
					.foregroundColor(.blue)
				
				Image(systemName: isMenuOpen ? "x.circle" : "plus.circle")
					.foregroundColor(.white)
					.font(.system(size: 22, weight: .bold, design: .rounded))
					.animation(.easeInOut(duration: 0.3), value: isMenuOpen)
			}
			.onTapGesture {
				isMenuOpen.toggle()
			}
		}
	}
}
 
struct DisplayOnOpenMenuViewModifier: ViewModifier {
	
	let isOpened: Bool
	let offset: CGFloat
	
	func body(content: Content) -> some View {
		content
			.shadow(color: .black.opacity(isOpened ? 0.1 : 0.0), radius: 10, x: 0, y: 5)
			.offset(y: isOpened ? offset : 0)
			.opacity(isOpened ? 100 : 0)
			.animation(.spring(response: 0.5, dampingFraction: 0.5, blendDuration: 0.5), value: isOpened)
	}
}
 
extension View {
	func displayOnMenuOpen(_ isOpened: Bool, offset: CGFloat) -> some View {
		modifier(DisplayOnOpenMenuViewModifier(isOpened: isOpened, offset: offset))
	}
}

SwiftUI makes it so easy to create beautiful animations and designs, and I believe it is the future of iOS development. In the next article, we will continue to build on what we created here, so stay tuned!

AJ Picard

Mobile Designer

MartianCraft is a US-based mobile software development agency. For nearly two decades, we have been building world-class and award-winning mobile apps for all types of businesses. We would love to create a custom software solution that meets your specific needs. Let's get in touch.