Skip to content

Instantly share code, notes, and snippets.

@BenLumenDigital
Created April 14, 2025 09:21
Show Gist options
  • Save BenLumenDigital/f51ea300536bf0298c736843b5fbfdce to your computer and use it in GitHub Desktop.
Save BenLumenDigital/f51ea300536bf0298c736843b5fbfdce to your computer and use it in GitHub Desktop.
SwiftUI Pacman loader
//
// PacManLoader.swift
// ScriptReader
//
// Created by Ben Harraway on 14/04/2025.
//
import SwiftUI
struct PacManLoader: View {
@State private var progress: CGFloat = 1.0
@State private var rotationAngle: Double = 0.0
let animationDuration = 0.3
let dotCount = 4
let dotSize: CGFloat = 15
let dotInitialOffset: CGFloat = 150.0
let dotFinalOffset: CGFloat = 0.0
let dotCycleDuration: TimeInterval = 1
let dotFadeInDuration: TimeInterval = 0.2
var body: some View {
ZStack {
HStack {
Color.gray.opacity(0.1)
}
// Dots
ForEach(0..<dotCount, id: \.self) { index in
AnimatingDot(
id: index,
size: dotSize,
initialOffset: dotInitialOffset,
finalOffset: dotFinalOffset,
cycleDuration: dotCycleDuration,
fadeInDuration: dotFadeInDuration,
delay: (dotCycleDuration / Double(dotCount)) * Double(index)
)
}
// PacMan
ZStack {
let gapSize = 1.0 - progress
let startTrim = gapSize / 2
let endTrim = 1.0 - (gapSize / 2)
Circle()
.trim(from: startTrim, to: endTrim)
.stroke(style: StrokeStyle(lineWidth: 50, lineCap: .butt, lineJoin: .round))
.foregroundColor(.gray)
}
.frame(width: 50, height: 50)
.padding(50)
.onAppear {
startPacManAnimations()
}
}
}
func startPacManAnimations() {
let pacManAnimation = Animation.linear(duration: animationDuration)
.repeatForever(autoreverses: true)
withAnimation(pacManAnimation) {
self.progress = 0.8
}
}
}
// MARK: - Animating Dot View
struct AnimatingDot: View {
let id: Int
let size: CGFloat
let initialOffset: CGFloat
let finalOffset: CGFloat
let cycleDuration: TimeInterval
let fadeInDuration: TimeInterval
let delay: TimeInterval
@State private var offset: CGFloat = 0.0
@State private var opacity: Double = 0.0
@State private var scaleEffect: CGFloat = 1.0
var body: some View {
Circle()
.fill(.gray)
.frame(width: size, height: size)
.scaleEffect(scaleEffect, anchor: .center)
.offset(x: offset)
.opacity(opacity)
.onAppear {
offset = initialOffset
scaleEffect = 1.0
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
animateDot()
}
}
}
private func animateDot() {
let moveDuration = max(0, cycleDuration - fadeInDuration)
let scaleDownDelay = moveDuration * 0.75
let scaleDownDuration = max(0, cycleDuration - scaleDownDelay)
offset = initialOffset
opacity = 0.0
scaleEffect = 1.0
withAnimation(.linear(duration: fadeInDuration)) {
opacity = 1.0
}
if moveDuration > 0 {
withAnimation(.linear(duration: moveDuration)) {
offset = finalOffset
}
}
withAnimation(.linear(duration: scaleDownDuration).delay(scaleDownDelay)) {
scaleEffect = 0.0
}
DispatchQueue.main.asyncAfter(deadline: .now() + cycleDuration) {
animateDot()
}
}
}
#Preview {
PacManLoader()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment