💫

SwiftUIでよくあるゲージっぽい見た目をつくる

2024/03/09に公開

基本的にはSliderなりGaugeなりを使用すればいいが、最低OS要件や柔軟にカスタムしたい場合に備えてゲージっぽい見た目を自前でつくる。

結論

他にいいやり方ありましたらご教示頂けますと幸いです!

struct GaugeView: View {
    let ratio: CGFloat

    var body: some View {
        Capsule()
            .foregroundStyle(.gray)
            .frame(height: 32)
            .overlay {
                GeometryReader { proxy in
                    Capsule()
                        .foregroundStyle(.black)
                        .frame(width: proxy.size.width * ratio)
                }
                .clipShape(Capsule())
            }
    }
}

パターン1:scaleEffectを使用するパターン

scaleEffectのxに割合を設定することで実現するパターン。

個人的にanchorという引数があることを見落としていたので、位置調整を気にしなくて良い実装がいいなと思っています。(後述しますが角丸をつけたい場合は話が変わります)

struct GaugeView1: View {
    let ratio: CGFloat

    var body: some View {
        Rectangle()
            .foregroundStyle(.gray)
            .frame(height: 32)
            .overlay {
                Rectangle()
                    .foregroundStyle(.black)
                    .scaleEffect(x: ratio, anchor: .leading)
            }
    }
}

パターン2:GeometryReaderを使用するパターン

最初に思いついたパターン。

悪くは無いと思いますが、trailingから伸びてくるようにしたい場合に少し追加実装が必要になってしまいます。

struct GaugeView2: View {
    let ratio: CGFloat

    var body: some View {
        Rectangle()
            .foregroundStyle(.gray)
            .frame(height: 32)
            .overlay {
                GeometryReader { proxy in
                    Rectangle()
                        .foregroundStyle(.black)
                        .frame(width: proxy.size.width * ratio)
                }
            }
    }
}

応用:角丸をつけたい場合

試行①

❌どうしてもRoundedRectangleかCapsuleを使用したい場合、パターン1の実装方法だとscaleEffectの影響をそのまま受け、角丸部分もスケールされてしまい端部の角丸が微妙に変形してしまいます。

struct GaugeView3: View {
    let ratio: CGFloat

    var body: some View {
        Capsule()
            .foregroundStyle(.gray)
            .frame(height: 32)
            .overlay {
                Capsule()
                    .foregroundStyle(.black)
                    .scaleEffect(x: ratio, anchor: .leading)
            }
    }
}

試行②

❌スケールするViewをRectangleに変えてclipShapeすることで一旦レイアウト崩れは防げますが、境目に角丸がつきません。

struct GaugeView4: View {
    let ratio: CGFloat

    var body: some View {
        Capsule()
            .foregroundStyle(.gray)
            .frame(height: 32)
            .overlay {
                Rectangle()
                    .foregroundStyle(.black)
                    .scaleEffect(x: ratio, anchor: .leading)
                    .clipShape(Capsule())
            }
    }
}

試行③

❌パターン2で実装する場合、余計なスケールが効いてしまうことがないので角丸部分は崩れません。ただしどうしても幅が狭くなった際に物理的にレイアウトが崩れてしまいます。

struct GaugeView5: View {
    let ratio: CGFloat

    var body: some View {
        Capsule()
            .foregroundStyle(.gray)
            .frame(height: 32)
            .overlay {
                GeometryReader { proxy in
                    Capsule()
                        .foregroundStyle(.black)
                        .frame(width: proxy.size.width * ratio)
                }
            }
    }
}

試行④

✅GeometryReaderを再度clipすることで幅が狭いときでも違和感のない形で実装できます。

struct GaugeView6: View {
    let ratio: CGFloat

    var body: some View {
        Capsule()
            .foregroundStyle(.gray)
            .frame(height: 32)
            .overlay {
                GeometryReader { proxy in
                    Capsule()
                        .foregroundStyle(.black)
                        .frame(width: proxy.size.width * ratio)
                }
                .clipShape(Capsule())
            }
    }
}

Discussion