Animations in SwiftUI with Examples — Part 3

Explore how we can implement animations in SwiftUI.
Dec 1 2022 · 6 min read

Background

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:

  1. Rotating Dot Animation —The dot rotates in the circle and works as a loader
  2. Three Bouncing Dot animation — 3 dots that bounce in the flow one by one
  3. Clock Animation — Self-explanatory right?

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!

Rotating Dot Animation

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 —

  1. Added 2 state properties, duration to observe the animation speed and the startAnimation boolean to start the animation.
  2. We are going to animate the given small ball to the edge of the circle in the whole round,
  • We have used 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.

Three Bouncing Dot animation

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 —

  1. 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.
  2. Hstack to place dots horizontally.
  3. 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!

Clock Animation

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 —

  1. LineType — Enum to keep different types of lines showing on the clock.
  • In which scale Keeps clock tick scale size.

2. Line struct is used to draw a line that conforms to Shape class.

  • In which we are drawing lines based on the parent view rect and the clock tick type and its scale.

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,

  1. Drew a circle on ZStack , and on that added a second tick

2. 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.

  • The only difference in the animation of both ticks will be that, a second indicator tick will be rotated faster than the minute indicator tick, which I’ve set six times faster than the minute tick.

That’s it!

Run code, we are done now. 🎉

To Conclude

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.


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development

Whether you need...

  • *
    High-performing mobile apps
  • *
    Bulletproof cloud solutions
  • *
    Custom solutions for your business.
Bring us your toughest challenge and we'll show you the path to a sleek solution.
Talk To Our Experts
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.