
Hi guys, today we are going to explore how we can implement animations in SwiftUI. The full source code of this project is available on Github, feel free to fork or directly use required animations in your applications.
This post consists of 3 animations:
The final implementation will look like this…

We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
We will first design static UI with tap functionality, which we will later animate.
Let’s add the following code to implement stepper UI
struct StepperAnimation: View {
@State var number: Int = 0
var body: some View {
ZStack {
Color.white
ZStack {
Text("\(number)")
.font(.system(size: 25))
HStack {
VStack {
}
.frame(width: 125, height: 100, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
onFlipPrevious()
}
VStack {
}
.frame(width: 125, height: 100, alignment: .trailing)
.contentShape(Rectangle())
.onTapGesture {
onFlipNext()
}
}
}
}
.frame(width: 250, height: 100, alignment: .center)
}
func onFlipPrevious() {
number -= 1
}
func onFlipNext() {
number += 1
}
}This snippet —
number variable that keeps track of the current numberZstack and Vstack and shows current number in the center while adding two tap detectors for handling previous and next step clickonFlipPrevious and onFlipNext functions on previous and next taps respectively. These functions just updates number without any animation.Pretty clear and easy right?
Now let’s add animation when stepper changes the number. We will use Animation.linear API for the animation.
We will start by adding a current rotation angle variable and then we will bind it to stepper view to rotate it with animation.
@State private var animatedAngle = 0.0Now let’s bind it to the view
var body: some View {
ZStack {
...
}
.frame(width: 250, height: 100, alignment: .center)
.rotation3DEffect(Angle(degrees: animatedAngle), axis: (x: 0.0, y: 1.0, z: 0.0))
.animation(Animation.linear(duration: 0.6), value: animatedAngle)
}Here we have used .animation API which takes two parameter.
We also used rotation3DEffect to rotate the views with respect to animatedAngle .
Now on button clicks, we need to update the animatedAngle with +180 and -180 degrees.
func onFlipPrevious() {
number -= 1
animatedAngle -= 180.0
}
func onFlipNext() {
number += 1
animatedAngle += 180.0
}That’s it. If you run the app now, animation will work but number will be updated instantly, we need to update it exactly when stepper reaches at half of the progress.
To fix that, we will have to keep two separate variables for front and back of the stepper. We will also add another variable isFront to keep track of which side of the stepper is shown currently.
@State var front: Int = 0
@State var back: Int = 0
@State var isFront = trueLet’s update view to use front and back variable instead of number
Text("\(isFront ? front : back)")Now we will have to update our previous and next functions to update front and back variable depending on the current side.
func onFlipPrevious() {
if(isFront) {
back = front - 1
} else {
front = back - 1
}
animatedAngle -= 180.0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
isFront = !isFront
}
}
func onFlipNext() {
if(isFront) {
back = front + 1
} else {
front = back + 1
}
animatedAngle += 180.0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
isFront = !isFront
}
}Here, we have updated isFront variable exactly after half progress, that is 0.3 seconds.
The last thing we need to do is rotate stepper view by 180 degree again to remove mirroring effect when back view is facing the user.
ZStack {
Text("\(isFront ? front : back)")
...
}
.rotation3DEffect(Angle(degrees : isFront ? 0 : 180), axis: (x: 0.0, y: 1.0, z: 0.0))
That’s it, run the app now and stepper should work completely fine.
struct HeartAnimation: View {
var body: some View {
VStack {
Image(systemName: "heart.fill")
.font(.system(size: 60))
.foregroundColor(Color.white)
}
}This animation consists of two views, heart jumping up and down, and a shadow.
We will first focus on creating heart view.
Let’s see basic code to add heart view without animation
struct HeartAnimation: View {
var body: some View {
VStack {
Image(systemName: "heart.fill")
.font(.system(size: 60))
.foregroundColor(Color.white)
}
}Pretty simple, right?
Now let’s add some animation! Add following variables to the view struct.
@State var transY: CGFloat = 0
var foreverAnimation =
Animation.linear.speed(0.3)
.repeatForever(autoreverses: true)transY variable will keep track of the jump translation on Y axis.foreverAnimation defines animation as linear with speed 0.3 and repeatForever makes it repeat forever.autoReverses flag determines how animation should repeated when it ends. As we are providing it true here, it will reverse the animation when it finishes.Now let’s bind these variables into View.
Image(systemName: "heart.fill")
...
.offset(x: 0, y: transY)
.onAppear {
withAnimation(foreverAnimation) {
transY = -25
}
}We start animation in onAppear callback where we change value of transY variable with animation. That will keep animating transY forever between it’s initial value 0 and new value -25 .
Now let’s implement shadow. We will start by adding a new variable that we will bind to shadow view.
@State var alpha: CGFloat = 1.0Initial value of the alpha will be 1.0 and we will animate it to 0.4 .
Now let’s add shadow view.
VStack {
Image(systemName: "heart.fill")
...
VStack{
}.frame(width: 50, height: 10, alignment: .center)
.background(Color.white)
.cornerRadius(10.0)
.opacity(alpha)
.scaleEffect(x: alpha, y: 1.0, anchor: UnitPoint.center)
.onAppear {
withAnimation(foreverAnimation) {
alpha = 0.4
}
}
}Pretty much the same as heart view except that here we bind new variable alpha to opacity and scaleEffect fields.
That’s it! Run the app and Heart animation should work amazingly well.
For wave animation, we have 3 waves. We will start by adding and animating 1 wave and later we will add other 2 waves.
Let’s add initial UI for 1 wave
struct WaveAnimation: View {
var body: some View {
ZStack {
Image(systemName: "circle.fill")
.font(.system(size: 60))
.foregroundColor(Color.white)
Image(systemName: "magnifyingglass")
.frame(width: 60, height: 60, alignment: .center)
.font(.system(size: 20))
.foregroundColor(Color.black)
.background(Color.white)
.cornerRadius(30)
}
}
}Pretty simple! We have a ZStack with a circle and a search icon on top of it.
Alright, now let’s animate it. We will start by adding required binding and animation variables — same as heart animation.
@State var scale1: CGFloat = 0
var foreverAnimation =
Animation.linear.speed(0.2)
.repeatForever(autoreverses: false)If you notice, we have set autoreverses to false here as we want wave to start again from initial position when it ends!
Let’s bind it to the views.
var body: some View {
ZStack {
Image(systemName: "circle.fill")
.font(.system(size: 60))
.foregroundColor(Color.white)
.opacity(1 - scale1)
.scaleEffect(1 + (scale1 * 2))
.onAppear {
withAnimation(foreverAnimation) {
scale1 = 1
}
}
...
}
}This snippet —
scale1 to 1 from initial 0 in withAnimation block. That means that variable will change value with animation instead of static change.scale1 variable to scaleEffect attribute of the wave. We have used equation 1 + (scale1 * 2) — That means scaleEffect will animate from 1 to 3 .scale1 variable to opacity attribute of the wave. We have used equation 1 — scale1— That means opacity will animate from 1 to 0 .That’s it! If you run the app, you will see 1 wave animating just fine!
Now let’s add other two waves.
var body: some View {
ZStack {
...
Image(systemName: "circle.fill")
.font(.system(size: 60))
.foregroundColor(Color.white)
.opacity(1 - scale2)
.scaleEffect(1 + (scale2 * 2))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation(foreverAnimation) {
scale2 = 1
}
}
}
Image(systemName: "circle.fill")
.font(.system(size: 60))
.foregroundColor(Color.white)
.opacity(1 - scale3)
.scaleEffect(1 + (scale3 * 2))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
withAnimation(foreverAnimation) {
scale3 = 1
}
}
}
...
}
}The trick here is to delay the animation for the other two waves by 0.5 second and 1 second respectively. The delay will keep waves separated enough time so that they look infinite when repeated.
Run the app now and enjoy the unlimited waves!
Here’s the 2nd part of this series.
That’s it for today, hope you learned something new! This article will give you enough knowledge to start digging into SwiftUI animation.
But by any means, this is not it! I will keep adding new animations in the github repository and planning to write at least 2–3 articles.
As always, suggestions and feedback are more than welcome.
Thanks for your support!

