Created
April 14, 2025 09:21
-
-
Save BenLumenDigital/f51ea300536bf0298c736843b5fbfdce to your computer and use it in GitHub Desktop.
SwiftUI Pacman loader
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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