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’sPath
. - The
WaveLoadingIndicator
uses theWave
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:
- Add the Lottie dependency to your project using Swift Package Manager.
- Create a
LottieLoadingView
that conforms toUIViewRepresentable
to wrap the Lottie animation view. - 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.