[SwiftUI]Hugginface Loading画面のAnimationを再現する

HuggingFaceのDemoを起動すると、3色のラインがProgressの周りをぐるぐる回るアニメーションが表示される。これがとても可愛らしいのでSwiftUIで再現してみました。



![Screen Recording 2023-07-19 at 10.34.04 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3352230/ec6200ca-ec01-11dc-7c39-33bc384fbb39.gif)

**実際のリンク**
https://huggingface.co/spaces/wyysf/GenMM


# 開発環境

Xcode : 14.3

Swift : 5.8

SwiftUIを使用

# 作成したアニメーション

実際のアニメーションでは、すぐに読み込まれてしまうため、以下の二点を追加しました。

- 100％になったら円に戻る。
- フェードアウト

![Screen Recording 2023-07-18 at 11.51.20 2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3352230/7d5ec5fe-fdb9-c022-993a-9b3365466240.gif)

# 実装コード

青：#3498DB　黄色：#F1C40F　赤：#E74C3C を使用してます。


```swift

import SwiftUI

struct HugginFaceProgressCircle: View {
    // 各円の回転度数、トリムの開始位置、トリムの終了位置を保持するためのプロパティ
    @State private var rotationDegrees: [Double] = [0, 0, 0]
    @State private var startTrim: [CGFloat] = [0, 0, 0]
    @State private var trimTo: CGFloat = 120.0 / 360.0 // 初期のトリム値（約33.3%）
    @State private var shouldRotate = true // 円が回転しているかどうかを示すフラグ
    @State private var opacity = 1.0 // テキストと円の不透明度
    
    let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect() // 0.1秒ごとにトリガされるタイマー
    
    // プログレスバーのパーセンテージ値と、それを更新するためのタイマー
    @State private var percentage: Int = 0
    let timer2 = Timer.publish(every: 0.3, on: .main, in: .common).autoconnect() // 0.3秒ごとにトリガされるタイマー
    
    // 緑の円を拡大するためのフラグ（未使用）
    @State private var expandGreenCircle = false
    
    // ビューの本体
    var body: some View {
        ZStack { // 複数のビューを重ねて表示
            Text("\(percentage)%") // 進行状況のパーセンテージを表示
                .font(.system(size: 30)) // フォントサイズを設定
                .opacity(opacity) // テキストの透明度を設定
                .onReceive(timer2) { _ in // timer2の更新毎に実行される
                    if percentage < 100 {
                        percentage += Int.random(in: 1...5) // パーセンテージをランダムに増加
                        if percentage >= 100 {
                            percentage = 100 // パーセンテージが100を超えないように調整
                            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                                withAnimation(.linear(duration: 1.0)) {
                                    trimTo = 1.0 // パーセンテージが100%に達したら全ての円を完全に塗りつぶす
                                    shouldRotate = false // パーセンテージが100%に達したら回転を停止
                                    expandGreenCircle = true // パーセンテージが100%に達したら緑の円を拡大（現状未使用）
                                }
                                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                                    withAnimation(.linear(duration: 0.4)) {
                                        self.opacity = 0 // パーセンテージが100%に達した1秒後にテキストと円を透明にする
                                    }
                                }
                            }
                        }
                    }
                }
            
            // 3つの円を描画
            ForEach(0..<3) { index in
                Circle()
                    .trim(from: 0, to: trimTo) // 円を一部切り取る
                    .stroke(lineWidth: 3) // 円の線の太さを設定
                    .frame(width: CGFloat(100 + 30 * index), height: CGFloat(100 + 30 * index)) // 円の大きさを設定
                    .foregroundColor(Color([ "Blue","Yellow","Red"][index])) // 円の色を設定
                    .rotationEffect(Angle.degrees(CGFloat(index*50)+rotationDegrees[index])) // 円を回転させる
                    .animation(shouldRotate ? Animation.linear(duration: 1).repeatForever(autoreverses: false) : .default) // 回転のアニメーション
                    .opacity(opacity) // 円の透明度を設定
            }
        }
        .onAppear() {
            startTrim = startTrim.map { _ in CGFloat.random(in: 0...1) } // ビューが表示されるときにトリムの開始位置をランダムに設定
        }
        .onReceive(timer) { _ in // timerの更新毎に実行される
            if shouldRotate {
                withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
                    // 内側の円ほど早く、外側の円ほど遅く回転します。
                    rotationDegrees = rotationDegrees.enumerated().map { index, degree in
                        degree + (50.0 * Double(1 - Double(index) * 0.2))
                    }
                }
            }
        }
    }
}

struct HugginFaceProgressCircle_Previews: PreviewProvider {
    static var previews: some View {
        HugginFaceProgressCircle()
    }
}


```

コメントなしの実装コード

https://gist.github.com/renyello/8107478347a6f7b77455d453e41b770b

# **まとめ**

Loading画面を自作するのも面白いですね。

今回のものでは、定期的に三本の線が重なるのが特にお気に入りです。

いいね、ブックマーク、フォローしていただけると勉強の励みになりますので是非お願いします。😉

追伸　--

Twitterで日々の学習風景を投稿してます。

[@Ren_yello](https://qiita.com/Ren_yello)