Hi guys, today we are going to explore how we can implement cool animations in SwiftUI.
This is the third part of the series of SwiftUI animation articles. If you haven’t already, please read the first and second articles.
This post consists of 3 animations:
The final implementation will look like this:
Not interested in the implementation? Check out the full source code of these animations on GitHub. Feel free to fork or directly use the required animations in your applications.
Alright!!
Let’s begin with the first animation.
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
This animation has 2 circles, the first one as an outline and the second one as a filled circle.
Let’s first start with adding the initial design.
struct RotatingDotAnimation: View {
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 4)
.foregroundColor(.white.opacity(0.5))
.frame(width: 150, height: 150, alignment: .center)
Circle()
.fill(.white)
.frame(width: 18, height: 18, alignment: .center)
.offset(x: -63)
}
}
}
Now, let’s see how we can animate these circles.
In this animation, we are going to animate the upper circle of the ZStack
.
struct RotatingDotAnimation: View {
@State private var startAnimation = false
@State private var duration = 1.0 // Works as speed, since it repeats forever
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 4)
.foregroundColor(.white.opacity(0.5))
.frame(width: 150, height: 150, alignment: .center)
Circle()
.fill(.white)
.frame(width: 18, height: 18, alignment: .center)
.offset(x: -63)
.rotationEffect(.degrees(startAnimation ? 360 : 0))
.animation(.easeInOut(duration: duration).repeatForever(autoreverses: false),
value: startAnimation
)
}
.onAppear {
self.startAnimation.toggle()
}
}
}
Here we have —
duration
to observe the animation speed and the startAnimation
boolean to start the animation.rotationEffect
modifier to rotate the ball at 360 degrees in the circle basically when the animation starts.3. That's not enough for animation, we have set animation that's all done by the animation
modifier to animate our animation.
easeInOut
— Is used to move the ball with ease, as you can see the animation starts and ends at a slow speed, and in between times it looks a bit faster according to the given speed duration
.If you want to see the difference, you can change the animation type .easeInOut
to .linear
, or any other.
repeatForever
— Is used for continuous animation and the autoreverses
argument is to disable the reverse animation when it ends one time.value
— Argument for monitoring the changes in the animation.That’s it!
Run your app now, you will see a small ball moving circularly in the circle boundary.
This animation has 3 dots.
Let’s first start by adding DotView
, that will be responsible for drawing a single dot.
private struct DotView: View {
@Binding var scale: CGFloat
var body: some View {
Circle()
.scale(scale)
.fill(.white.opacity(scale >= 0.7 ? scale : scale - 0.1))
.frame(width: 50, height: 50, alignment: .center)
}
}
Here, we have added scale
as a binding, as we are going to change that property to animate the dot's appearance.
Now let’s see how we can animate those dots!
If you observe animation, we have to add a property that will be different for each dot — Delay.
The second and third dots are delayed to look like bouncing balls. Also, they translate and scale a little bit more compared to the first dot.
Let’s add a struct and an array of struct data to store this information.
struct AnimationData {
var delay: TimeInterval
}
static let DATA = [
AnimationData(delay: 0.0),
AnimationData(delay: 0.2),
AnimationData(delay: 0.4),
]
Now let’s have a look at the root view.
struct ThreeBounceAnimation: View {
@State var scales: [CGFloat] = DATA.map { _ in return 0 }
var animation = Animation.easeInOut.speed(0.5)
var body: some View {
HStack {
DotView(scale: .constant(scales[0]))
DotView(scale: .constant(scales[1]))
DotView(scale: .constant(scales[2]))
}
.onAppear {
animateDots() // Not defined yet
}
}
}
Here we have —
scales
for each dot and set it to 0. This variable is responsible for animating the scale of dots as we have bound it to scale
of the circle in DotView
.Hstack
to place dots horizontally.animateDots
function to start animation as soon as the view is visible.If you comment out animateDots
call and run the code, it will show 3 static dots. Now let’s implement the animation function.
func animateDots() {
for (index, data) in Self.DATA.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + data.delay) {
animateDot(binding: $scales[index], animationData: data)
}
}
//Repeat
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
animateDots()
}
}
func animateDot(binding: Binding<CGFloat>, animationData: AnimationData) {
withAnimation(animation) {
binding.wrappedValue = 1
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
withAnimation(animation) {
binding.wrappedValue = 0.2
}
}
}
Here, animateDots
is responsible for the main loop and animateDot
is responsible for the animation of a single dot.
As we can see, animateDots
calls animateDot
with delay for each view as per AnimationData
. That means the second and third dots will start animating late for each iteration.
After 1 second, the animateDots
function calls itself to repeat the animation.
That’s it!
Run the code, you will see beautiful dots scaling and visible in rhythm!
First, start with designing the clock.
Drawing the outer clock circle should not need anything extra but compared to that drawing the line is much more complex.
Let’s start with the line view.
public struct Line: Shape {
var lineType: LineType
init(type: LineType) {
self.lineType = type
}
public func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: center)
path.addLine(to: CGPoint(x: rect.width * lineType.scale, y: rect.width * lineType.scale))
return path
}
}
enum LineType {
case minute
case second
var scale: CGFloat {
switch self {
case .minute:
return 0.3
case .second:
return 0.2
}
}
}
Here we have —
LineType
— Enum to keep different types of lines showing on the clock.scale
Keeps clock tick scale size.2. Line
struct is used to draw a line that conforms to Shape
class.
Now, let’s combine all the things in the main view.
Our main view will look like this,
struct ClockAnimation: View {
@State private var duration = 1.2
@State private var startAnimation = false
var body: some View {
ZStack {
Circle()
.stroke(.black, lineWidth: 2)
.background(Circle().fill(.white))
Line(type: .second)
.stroke(Color.blue, style: StrokeStyle(lineWidth: 5.5, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(startAnimation ? 360 : 0))
.animation(.linear(duration: duration).repeatForever(autoreverses: false), value: startAnimation)
Line(type: .minute)
.stroke(Color.primary, style: StrokeStyle(lineWidth: 6, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(startAnimation ? 360 : 0))
.animation(.linear(duration: duration * 6).repeatForever(autoreverses: false), value: startAnimation)
}
.frame(width: 150, height: 150, alignment: .center)
.onAppear {
self.startAnimation.toggle()
}
}
}
Let’s understand this in detail.
We have done it in two steps,
ZStack
, and on that added a second tick2. At the top of a second tick, set a small minute tick
But only adding a view is not enough we have to set the line(tick) rotation angle and its animation.
.rotationEffect
— As we are making an animation of a clock our line will be rotated 360 degrees in the clockwise direction after starting the animation.
.animation
— We need linear animation as the clock tick always rotates in the linear mode and also it’ll be in the repeatForever
mode with the autoreverses
off.
That’s it!
Run code, we are done now. 🎉
That’s it for today. Hope you learned something new!
This just touches the base of the SwiftUI animations. It’s possible to design much more complex animations using Canvas, animations API, and Math.
As always, your suggestions and feedback give me fuel to keep writing, add them in the comment section below.
Whether you need...