概要
以前作成したローディングアニメーションを整理したのでコピペして使えるように残しておきます。
当時はiOS15よりも前だったのでanimation()
をwithAnimation
に書き直しました。
今回紹介するローディングアニメーションは以下の内容です。
主に以下の用途で使える内容となっています。
- 処理中であることがアプリユーザーにわかるような画面を作りたい
- 画面に動作中であると伝わるような動きを見せたい
この後の詳細セクションで以下のgif動画のアニメーションを表示しているViewと各カスタムローディングアニメーションのコードを紹介しています。
詳細
gif動画で表示している画面です。
ContentView.swift
import SwiftUI
struct ContentView: View {
@State private var rotation: Double = 0.0
var body: some View {
VStack {
Spacer()
CircleLoader()
.frame(width: 50, height: 50)
Spacer()
}
VStack {
Spacer()
ParticleLoader()
.frame(width: 50, height: 50)
Spacer()
}
VStack {
Spacer()
BouncingDotsLoader()
.frame(width: 50, height: 50)
Spacer()
}
}
}
上のアニメーション
CircleLoader.swift
import SwiftUI
struct CircleLoader: View {
@State private var rotation: Double = 0.0
var body: some View {
Circle()
.trim(from: 0.1, to: 1.0)
.stroke(style: StrokeStyle(lineWidth: 8, lineCap: .round, lineJoin: .round))
.foregroundColor(.blue)
.rotationEffect(Angle(degrees: rotation))
.onAppear {
withAnimation(Animation.linear(duration: 4.0).repeatForever(autoreverses: false)) {
rotation = 360
}
}
}
}
真ん中のアニメーション
ParticleLoader
import SwiftUI
struct Particle: Identifiable {
let id: UUID
let angle: Double
}
struct ParticleLoader: View {
let particleCount: Int = 8
let radius: CGFloat = 20.0
@State private var particles: [Particle] = []
var body: some View {
ZStack {
ForEach(particles) { particle in
Circle()
.frame(width: 10, height: 10)
.foregroundColor(.blue)
.offset(x: radius * CGFloat(cos(particle.angle * .pi / 180)),
y: radius * CGFloat(sin(particle.angle * .pi / 180)))
}
}
.onAppear {
particles = (0..<particleCount).map { index in
Particle(id: UUID(), angle: Double(index) * (360.0 / Double(particleCount)))
}
withAnimation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false)) {
particles = particles.map { particle in
Particle(id: particle.id, angle: particle.angle - 600.0)
}
}
}
}
}
下のアニメーション
DotLoader
struct BouncingDotsLoader: View {
@State private var offsetY1: CGFloat = 0.0
@State private var offsetY2: CGFloat = 0.0
@State private var offsetY3: CGFloat = 0.0
let dotSize: CGFloat = 20.0
let bounceDistance: CGFloat = 20.0
let animationDuration: Double = 0.5
let delayBetweenDots: Double = 0.2
var body: some View {
HStack(spacing: 20) {
DotView()
.offset(y: offsetY1)
DotView()
.offset(y: offsetY2)
DotView()
.offset(y: offsetY3)
}
.onAppear {
animateDots()
}
}
private func animateDots() {
withAnimation(Animation.easeInOut(duration: animationDuration).repeatForever(autoreverses: true)) {
offsetY1 = bounceDistance
}
DispatchQueue.main.asyncAfter(deadline: .now() + delayBetweenDots) {
withAnimation(Animation.easeInOut(duration: animationDuration).repeatForever(autoreverses: true)) {
offsetY2 = bounceDistance
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2 * delayBetweenDots) {
withAnimation(Animation.easeInOut(duration: animationDuration).repeatForever(autoreverses: true)) {
offsetY3 = bounceDistance
}
}
}
}
struct DotView: View {
let dotSize: CGFloat = 20.0
var body: some View {
Circle()
.frame(width: dotSize, height: dotSize)
.foregroundColor(.blue)
}
}