[Xcode/SwiftUI] よくあるCircleで利用状況を表示するやつを実装する方法

実装

import SwiftUI

struct HomeView: View {
    @State private var currentUsagemount: Double = 25_000
    @State private var maxUsageAmount: Double = 50_000
    @State private var isProgressAnimating = false

    private var progressPercentage: Double {
        return min(currentUsagemount / maxUsageAmount, 1.0) // 100%より上の限界突破ブロックのため
    }

    var body: some View {
        VStack(spacing: 40) {
            topSection()
            // 円形プログレスバー
            ZStack {
                circleView()
                middleSection()
            }

            // 統計情報
            VStack(spacing: 16) {
                staticTopSection()
                Divider()
                staticControlSection()
            }
            .padding()
            .background(Color(.systemGray6))
            .cornerRadius(12)
            .padding(.horizontal)
            Spacer()
        }
        .padding()
        .onAppear {
            isProgressAnimating = true
        }
    }

    @ViewBuilder
    private func topSection() -> some View {
        Text("利用状況")
            .font(.title2)
            .fontWeight(.medium)
            .foregroundColor(.gray)
    }

    @ViewBuilder
    private func middleSection() -> some View {
        VStack(spacing: 8) {
            Text("今月の利用額")
                .font(.caption)
                .foregroundColor(.gray)

            Text("¥\(Int(currentUsagemount).formatted())")
                .font(.system(size: 32, weight: .bold, design: .rounded))
                .foregroundColor(.primary)

            Text("/ ¥\(Int(maxUsageAmount).formatted())")
                .font(.caption)
                .foregroundColor(.gray)
        }
    }

    @ViewBuilder
    private func staticTopSection() -> some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                Text("利用率")
                    .font(.caption)
                    .foregroundColor(.gray)
                Text("\(Int(progressPercentage * 100))%")
                    .font(.title3)
                    .fontWeight(.semibold)
            }

            Spacer()

            VStack(alignment: .trailing, spacing: 4) {
                Text("残り利用可能額")
                    .font(.caption)
                    .foregroundColor(.gray)
                Text("¥\(Int(maxUsageAmount - currentUsagemount).formatted())")
                    .font(.title3)
                    .fontWeight(.semibold)
                    .foregroundColor(.green)
            }
        }
        .padding(.horizontal)
    }

    @ViewBuilder
    private func staticControlSection() -> some View {
        VStack(spacing: 16) {
            Text("金額を変更")
                .font(.headline)

            HStack(spacing: 20) {
                Button("¥10,000") {
                    withAnimation(.easeInOut(duration: 0.8)) {
                        currentUsagemount = 10_000
                    }
                }
                .buttonStyle(.bordered)

                Button("¥25,000") {
                    withAnimation(.easeInOut(duration: 0.8)) {
                        currentUsagemount = 25_000
                    }
                }
                .buttonStyle(.bordered)

                Button("¥40,000") {
                    withAnimation(.easeInOut(duration: 0.8)) {
                        currentUsagemount = 40_000
                    }
                }
                .buttonStyle(.bordered)
            }

            Slider(value: $currentUsagemount, in: 0...maxUsageAmount, step: 1_000)
                .padding(.horizontal)
        }
    }

    @ViewBuilder
    private func circleView() -> some View {
        // 背景の円
        Circle()
            .stroke(Color.gray.opacity(0.2), lineWidth: 12)
            .frame(width: 250, height: 250)

        // プログレス円
        Circle()
            .trim(from: 0, to: isProgressAnimating ? progressPercentage : 0)
            .stroke(
                LinearGradient(
                    colors: [Color.blue.opacity(0.7), Color.cyan],
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                ),
                style: StrokeStyle(lineWidth: 12, lineCap: .round)
            )
            .frame(width: 250, height: 250)
            .rotationEffect(.degrees(-90))
            .animation(.easeInOut(duration: 1.5), value: isProgressAnimating)

    }
}