When your app is executing a task which will take a little longer, of course you do not execute this task on the main thread.

Nevertheless there are situations, especially during first time launches you need to show some kind of progress before you can render the view, for example you are waiting for an API to have loaded some initial data.

Let’s do it

In this post I’m gonna show you the code for a custom build AnimatedProgressView in SwiftUI which accepts two parameters.

The first parameter is the numberOfDots and the second is the color.

How does it work?

In the body DotView objects are shown in a HStack for the numberOfDots that you have set.

The DotView draws a Circle with the color you have set and performing a scaleEffect which is passed.

The scales are defined in the initializer.

The full code is show here:


import SwiftUI

struct AnimatedProgressView: View {
    @State private var scales: [CGFloat]
    private let data: [AnimationData]
    private var animation = Animation.easeInOut.speed(0.5)
    private var color: Color
    
    init(numberOfDots: Int = 3, color: Color = .green) {
        self.color = color
        _scales = State(initialValue: Array(repeating: 0, count: numberOfDots))
        data = Array(repeating: AnimationData(delay: 0.2), count: numberOfDots).enumerated().map { (index, data) in
            var modifiedData = data
            modifiedData.delay *= Double(index)
            return modifiedData
        }
    }
    
    func animateDots() {
        for (index, data) in 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
            }
        }
    }
    
    var body: some View {
        VStack {
            HStack {
                ForEach(0..<scales.count, id: \.self) { index in
                    DotView(scale: $scales[index], color: color)
                }
            }
        }
        .onAppear {
            animateDots()
        }
    }
}

struct AnimationData {
    var delay: TimeInterval
}

private struct DotView: View {
    @Binding var scale: CGFloat
    var color: Color

    var body: some View {
        Circle()
            .fill(color.opacity(scale >= 0.7 ? 1 : 0.7))
            .scaleEffect(scale)  
            .frame(width: 50, height: 50, alignment: .center)
    }
}

How to use it?

It could not have been simpeler as this.

struct ContentView: View {
    var body: some View {
        VStack {
            AnimatedProgressView(numberOfDots: 3, color:.blue)
        
        }
        .padding()
    }
}

You simply define the view with the parameters in the View in which you want to display it and that’s it.

How to include?

You just copy and paste. I don’t like simple solutions like this in Swift packages since you often want to tweak them a little to your personal needs which is way easier to do when you have the source code in your Xcode Project.

Tags

Comments are closed