[SwiftUI] ZStackとCircleを組み合わせて円形のプログレスバーを作る

本記事では「円形のプログレスバー」をSwiftUIで作る実装を紹介します。
2022.12.05

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは。CX事業本部の平屋です。

本記事では、以下のような「円形のプログレスバー」をSwiftUIで作る実装を紹介します。

検証環境

  • macOS Monterey 12.6
  • Xcode Version 13.4

ベースの作成

まずはビューを2つ作成します。

  • CircularProgressBar: プログレスバーのビュー
  • ContentView: プログレスバーを使うビュー
// プログレスバーのビュー
struct CircularProgressBar: View {
    @Binding var progress: CGFloat

    var body: some View {
        ZStack {
            
        }
    }
}

// プログレスバーを使うビュー
struct ContentView: View {
    @State var progressValue: CGFloat = 0.3

    var body: some View {
        VStack {
            CircularProgressBar(progress: $progressValue)
                .frame(width: 150.0, height: 150.0)
                .padding(32.0)
            Spacer()
        }
    }
}

今回紹介するプログレスバーの実装では、以下の3つをCircularProgressBar内のZStackに重ねて実現します。

  • 背景の円
  • 進捗を示す円
  • 進捗率のテキスト

背景の円の作成

まずは、CircularProgressBar.bodyZStackに「背景の円」を追加します。

円を追加

ZStackCircle()を追加すると、塗りつぶしの円が描画されます。

// プログレスバーのビュー
struct CircularProgressBar: View {
    @Binding var progress: CGFloat

    var body: some View {
        ZStack {
            // 背景の円
            Circle()
        }
    }
}

スタイルの適用

円形の線を描画するように修正します。

ZStack {
    // 背景の円
    Circle()
        // 円形の線を描画するように指定
        .stroke(lineWidth: 24.0)
        .opacity(0.3)
        .foregroundColor(.blue)
}

「背景の円」はこれで完成です。

進捗を示す円の作成

次に、「進捗を示す円」をZStackの手前側に追加します。

円を追加

ZStackCircle()を追加します。背景の円と異なり、strokeモディファイアにStrokeStyleを与えて線の端の形状などを指定しています。

ZStack {
    // 背景の円
    // ...

    // 進捗を示す円
    Circle()
        // 線の端の形状などを指定
        .stroke(style: StrokeStyle(lineWidth: 24, lineCap: .round, lineJoin: .round))
        .foregroundColor(.blue)
}

この時点では2つの円が同じ形状になっていて「背景の円」が全て隠れている状態です。

進捗分だけ描画されるようにする

進捗分だけ描画されるように修正します。

ZStack {
    // 背景の円
    // ...

    // 進捗を示す円
    Circle()
        // 始点/終点を指定して円を描画する
        // 始点/終点には0.0-1.0の範囲に正規化した値を指定する
        .trim(from: 0.0, to: min(progress, 1.0))
        // 線の端の形状などを指定
        // ...
}

現時点の描画結果は以下のようになります。(円のデフォルトの原点は「時計の3時の位置」のようです。)

円の原点を修正する

rotationEffectモディファイアを使って円の原点を修正します。

ZStack {
    // 背景の円
    // ...

    // 進捗を示す円
    Circle()
        // ...
        .foregroundColor(.blue)
        // デフォルトの原点は時計の12時の位置ではないので回転させる
        .rotationEffect(Angle(degrees: 270.0))
}

期待する表示になりました。「進捗を示す円」はこれで完成です。

進捗率のテキストの作成

最後に、「進捗率のテキスト」をZStackの手前側に追加します。

ZStack {
    // 背景の円
    Circle()
        // ...

    // 進捗を示す円
    Circle()
        // ...

    // 進捗率のテキスト
    Text(String(format: "%.0f%%", min(progress, 1.0) * 100.0))
        .font(.largeTitle)
        .bold()
}

期待する表示になりました。これで完成です。

まとめ

完成版のコード全体は以下の通りです。

// プログレスバーのビュー
struct CircularProgressBar: View {
    @Binding var progress: CGFloat

    var body: some View {
        ZStack {
            // 背景の円
            Circle()
                // ボーダーラインを描画するように指定
                .stroke(lineWidth: 24.0)
                .opacity(0.3)
                .foregroundColor(.blue)

            // 進捗を示す円
            Circle()
                // 始点/終点を指定して円を描画する
                // 始点/終点には0.0-1.0の範囲に正規化した値を指定する
                .trim(from: 0.0, to: min(progress, 1.0))
                // 線の端の形状などを指定
                .stroke(style: StrokeStyle(lineWidth: 24, lineCap: .round, lineJoin: .round))
                .foregroundColor(.blue)
                // デフォルトの原点は時計の12時の位置ではないので回転させる
                .rotationEffect(Angle(degrees: 270.0))

            // 進捗率のテキスト
            Text(String(format: "%.0f%%", min(progress, 1.0) * 100.0))
                .font(.largeTitle)
                .bold()
        }
    }
}

// プログレスバーを使うビュー
struct ContentView: View {
    @State var progressValue: CGFloat = 0.3

    var body: some View {
        VStack {
            CircularProgressBar(progress: $progressValue)
                .frame(width: 150.0, height: 150.0)
                .padding(32.0)
            Spacer()
        }
    }
}

参考資料