Creating a Custom Loading Indicator in SwiftUI

In modern iOS app development with SwiftUI, providing visual feedback to users during loading or processing tasks is essential for a smooth user experience. A custom loading indicator can significantly enhance the app’s aesthetics and align with the brand identity. SwiftUI offers powerful tools to create animated, custom loading indicators that are both engaging and informative.

Why Use a Custom Loading Indicator?

  • Branding: A custom indicator can match the app’s unique design and branding.
  • User Experience: Enhances the perceived performance of the app.
  • Engagement: An animated loading indicator keeps the user engaged while waiting.

How to Create a Custom Loading Indicator in SwiftUI

Here’s a comprehensive guide on creating a custom loading indicator in SwiftUI:

Step 1: Basic Circular Loading Indicator

Let’s start by creating a basic circular loading indicator using SwiftUI’s Circle and trim modifier.

import SwiftUI

struct CircularLoadingView: View {
    @State private var isAnimating = false

    var body: some View {
        Circle()
            .trim(from: 0.0, to: 0.8)
            .stroke(AngularGradient(gradient: Gradient(colors: [.red, .orange]), center: .center), style: StrokeStyle(lineWidth: 4, lineCap: .round))
            .frame(width: 50, height: 50)
            .rotationEffect(Angle(degrees: isAnimating ? 360 : 0))
            .animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: isAnimating)
            .onAppear {
                isAnimating = true
            }
    }
}

struct CircularLoadingView_Previews: PreviewProvider {
    static var previews: some View {
        CircularLoadingView()
    }
}

Explanation:

  • Circle() creates a circle shape.
  • trim(from: 0.0, to: 0.8) trims the circle to create a partial arc.
  • stroke adds a stroke with a gradient color and rounded line caps.
  • rotationEffect rotates the circle to create an animation.
  • animation applies a linear, repeating animation to the rotation.
  • onAppear starts the animation when the view appears.

Step 2: Modifying the Circle with Stroke and Color

Customize the appearance further by changing the stroke style and colors.

struct CustomLoadingView: View {
    @State private var animate = false

    var body: some View {
        Circle()
            .stroke(lineWidth: 5.0)
            .opacity(0.3)
            .frame(width: 50, height: 50)
            .overlay(
                Circle()
                    .trim(from: 0, to: 0.5)
                    .stroke(style: StrokeStyle(lineWidth: 5.0, lineCap: .round, lineJoin: .round))
                    .rotationEffect(Angle(degrees: animate ? 360 : 0))
                    .animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: animate)
            )
            .onAppear() {
                animate = true
            }
    }
}

struct CustomLoadingView_Previews: PreviewProvider {
    static var previews: some View {
        CustomLoadingView()
    }
}

This example overlays two circles to give a more detailed loading effect. The upper circle animates, while the lower circle remains static.

Step 3: Creating a Dashed Loading Indicator

Another common type is a dashed loading indicator. This can be achieved using an array of dashes in the stroke style.

struct DashedLoadingView: View {
    @State private var animate = false

    var body: some View {
        Circle()
            .trim(from: 0, to: 0.7)
            .stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, dash: [12, 8]))
            .rotationEffect(Angle(degrees: animate ? 360 : 0))
            .animation(Animation.linear(duration: 2).repeatForever(autoreverses: false), value: animate)
            .frame(width: 60, height: 60)
            .onAppear() {
                animate = true
            }
    }
}

struct DashedLoadingView_Previews: PreviewProvider {
    static var previews: some View {
        DashedLoadingView()
    }
}

In this code:

  • dash: [12, 8] creates a dashed line with a dash length of 12 and a gap of 8.
  • The animation continuously rotates the dashed circle.

Step 4: Implementing a Pulsating Dot Loading Indicator

Pulsating dot indicators are subtle yet effective. Create this using scaleEffect and opacity animations.

struct PulsatingDot: View {
    @State private var isAnimating = false

    var body: some View {
        Circle()
            .frame(width: 20, height: 20)
            .foregroundColor(.blue)
            .scaleEffect(isAnimating ? 1.0 : 0.5)
            .opacity(isAnimating ? 1.0 : 0.5)
            .animation(Animation.easeInOut(duration: 0.5).repeatForever(autoreverses: true), value: isAnimating)
            .onAppear {
                isAnimating = true
            }
    }
}

struct PulsatingDotsLoadingView: View {
    var body: some View {
        HStack {
            PulsatingDot()
            PulsatingDot().delay(0.2)
            PulsatingDot().delay(0.4)
        }
    }
}

struct PulsatingDotsLoadingView_Previews: PreviewProvider {
    static var previews: some View {
        PulsatingDotsLoadingView()
    }
}

This indicator consists of three dots that pulse in sequence. Each dot animates in scale and opacity with a slight delay to create a wave effect.

Step 5: Animating Custom Shapes

For unique indicators, you can animate custom shapes using SwiftUI’s Path. Here’s an example with a custom wave shape.

struct WaveLoadingIndicator: View {
    @State private var phase: CGFloat = 0.0

    var body: some View {
        ZStack {
            Wave(phase: phase, amplitude: 10, frequency: 1.5)
                .stroke(Color.blue, lineWidth: 2)
                .frame(width: 100, height: 50)
        }
        .onAppear {
            withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
                phase = .pi * 2
            }
        }
    }
}

struct Wave: Shape {
    var phase: CGFloat
    var amplitude: CGFloat
    var frequency: CGFloat

    func path(in rect: CGRect) -> Path {
        var path = Path()

        let width = rect.width
        let height = rect.height

        let midHeight = height / 2

        let wavelength = width / frequency

        for x in 0...Int(width) {
            let relativeX = CGFloat(x) / width
            let y = amplitude * sin(2 * .pi * relativeX * frequency + phase) + midHeight

            if x == 0 {
                path.move(to: CGPoint(x: CGFloat(x), y: y))
            } else {
                path.addLine(to: CGPoint(x: CGFloat(x), y: y))
            }
        }
        return path
    }
}

struct WaveLoadingIndicator_Previews: PreviewProvider {
    static var previews: some View {
        WaveLoadingIndicator()
    }
}

Explanation:

  • The Wave struct creates a custom wave shape using SwiftUI’s Path.
  • The WaveLoadingIndicator uses the Wave shape and animates the phase to create a moving wave effect.

Step 6: Using Third-Party Libraries

Consider leveraging existing SwiftUI animation libraries like Lottie for complex animations. Lottie supports importing Adobe After Effects animations.

import SwiftUI
import Lottie

struct LottieLoadingView: UIViewRepresentable {
    let name: String
    let loopMode: LottieLoopMode

    func makeUIView(context: Context) -> LottieAnimationView {
        let animationView = LottieAnimationView(name: name)
        animationView.loopMode = loopMode
        animationView.play()
        return animationView
    }

    func updateUIView(_ uiView: LottieAnimationView, context: Context) {
        // Do nothing
    }
}

struct ContentView: View {
    var body: some View {
        LottieLoadingView(name: "loadingAnimation", loopMode: .loop) // Replace "loadingAnimation" with your Lottie file name
            .frame(width: 200, height: 200)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

To use Lottie:

  1. Add the Lottie dependency to your project using Swift Package Manager.
  2. Create a LottieLoadingView that conforms to UIViewRepresentable to wrap the Lottie animation view.
  3. Place your Lottie JSON file in your project and reference it by name.

Conclusion

Creating custom loading indicators in SwiftUI is a powerful way to enhance your app’s branding and user experience. Whether it’s a simple rotating circle, a pulsating dot, or a complex Lottie animation, SwiftUI provides the flexibility to design engaging and informative loading animations. By following these steps, you can implement unique loading indicators that perfectly match your app’s style and functionality.