5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUIでのカスタムローディングアニメーションサンプル

Last updated at Posted at 2023-11-29

概要

以前作成したローディングアニメーションを整理したのでコピペして使えるように残しておきます。
当時はiOS15よりも前だったのでanimation()withAnimationに書き直しました。

今回紹介するローディングアニメーションは以下の内容です。

主に以下の用途で使える内容となっています。

  • 処理中であることがアプリユーザーにわかるような画面を作りたい
  • 画面に動作中であると伝わるような動きを見せたい

この後の詳細セクションで以下のgif動画のアニメーションを表示しているViewと各カスタムローディングアニメーションのコードを紹介しています。

Simulator Screen Recording - iPhone SE (3rd generation) - 2023-11-29 at 18.58.18.gif


詳細

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)
    }
}
5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?