Creating a Button Animation in SwiftUI
Today we’re going to look at how to create a quick and easy animation for a button that scales and fades out when you tap on it.
Setting Up
For this tutorial we’re going to need two main views:
The button itself
A view for handling the animation
So to get started let’s create a simple rectangular button.
Button(action: {
// button action
}) {
ZStack {
Rectangle()
.frame(width: 120, height: 60)
.foregroundColor(.red)
Text("Button")
.foregroundColor(.white)
}
}
Animation state
Now that we’ve got a button, we need to add some way to manage its current animation state. The goal here is to have some way to notify our animation view that the button was tapped, then when the animation is done that state should reset.
An easy way to do that is to have a bool for keeping track of if the button was tapped, then after a delay that is the same duration as the animation just reset the bool. Assuming we want our animation to last for 0.5 seconds, let’s go ahead and add a state property for our button and update the action
for our button to toggle that property.
@State private var didTapButton = false
Button(action: {
self.didTapButton = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.didTapButton = false
}
})
Creating the animation
OK cool, so we have a button that keeps track of if it was tapped. Now we’re ready to build out the animation for the button. The animation we’re going to use is very simple - just a rectangle that matches the button’s color and size that expands out and fades out at the same time.
Start by creating a new view called ButtonAnimation
that has a binding property for the state from the main button view. You can also add in a Color property to make sure the animation’s color matches the button’s color.
struct ButtonAnimation: View {
@Binding var isExpanded: Bool
let color: Color
var body: some View {
Rectangle()
.frame(width: 120, height: 60)
.foregroundColor(color)
}
}
Next up is handling the animation based on the isExpanded
binding. To do that we’ll need to update two different modifiers:
The animation view’s alpha should go from 0.5 to 0.0 alpha so it fades out
The animation view’s scale should go from 1 to 2.5 so it expands
Rectangle()
.frame(width: 120, height: 60)
.foregroundColor(color)
.opacity(isExpanded ? 0 : 0.5)
.scaleEffect(isExpanded ? 2.5 : 1)
If we were to run this now you wouldn’t ever see the animation view because nothing is being told to animate. Instead, the animation view would simply toggle between its start and end state, and since its end state has an opacity of 0 it would be invisible in the expanded state.
All we need to do is tell the animation view how to animate with an animation modifier.
.animation(isExpanded ? .easeOut(duration: 0.5) : nil)
I went with an easeOut animation curve, but you can use whatever you’d like. You may have noticed that I’m also skipping animation when isExpanded
is reset to false. This is because I don’t want the user to see the reverse animation resetting the animation view. Instead the animation view should just reset so it’s ready to go again without the user ever knowing.
Putting it all together
Last but not least we need to add the animation to our main button view. The animation should look like it expands out from behind the button, so go ahead and add it as the first item in the ZStack
:
ButtonAnimation(isExpanded: $didTapButton, color: .red)
Now when you tap the button you should see your fancy new animation run!
tl;dr here’s the full code
struct ButtonAnimation: View {
@Binding var isExpanded: Bool
let color: Color
var body: some View {
Rectangle()
.frame(width: 120, height: 60)
.foregroundColor(color)
.opacity(isExpanded ? 0 : 0.5)
.scaleEffect(isExpanded ? 2.5 : 1)
.animation(isExpanded ? .easeOut(duration: 0.5) : nil)
}
}
struct ContentView: View {
@State private var didTapButton = false
var body: some View {
Button(action: {
self.didTapButton = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.didTapButton = false
}
}) {
ZStack {
ButtonAnimation(isExpanded: $didTapButton, color: .red)
Rectangle()
.frame(width: 120, height: 60)
.foregroundColor(.red)
Text("Button")
.foregroundColor(.white)
}
}
}
}