【SwiftUI】独自のGaugeStyleを作る

SwiftUI

GaugeはGaugeStyleで見た目を変更する事ができました。
このGaugeStyleは自分で定義する事もでき、好みのGaugeを作る事ができます。

1度しか使わない場合はGaugeやGaugeStyleに頼らない形で作っても良いと思いますが、
GaugeStyleを作ってしまえば使い回しが容易になるので是非覚えていって下さい。

Gaugeの基本についてはこちらの記事をご覧ください。

GaugeStyleを定義する

Gaugeのサンプル

GaugeStyleを使用する前にGaugeのサンプルコードを紹介します。
今回はこのGaugeをベースにGaugeStyleを適用して行きます。

struct MyGaugeStyleSampleView: View {
    let currentValue = 85.0
    let minValue = 0.0
    let maxValue = 150.0
    
    var body: some View {
        VStack {
            Gauge(value: currentValue ,in: minValue...maxValue){
                Text("Sample Gauge")
            } currentValueLabel: {
                Text("\(Int(currentValue))")
            } minimumValueLabel: {
                Text("\(Int(minValue))").foregroundColor(.blue)
            } maximumValueLabel: {
                Text("\(Int(maxValue))").foregroundColor(.red)
            }
            .gaugeStyle(.linearCapacity)
        }.padding()
    }
}

GaugeStyleプロトコルを実装する

まずは構造体にGaugeStyleプロトコルを実装します。

struct SampleGaugeStyle:GaugeStyle{
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
    }
}

この様にGaugeStyleプロトコルを実装するとmakeBodyメソッドが必要となります。
makeBodyメソッドで返されるViewがそのままGaugeとして表示されます。

試しにこのまま適用してみましょう。
先程の.linearCapacityをSampleGaugeStyleに変更しました。

Gauge(value: currentValue ,in: minValue...maxValue){
    Text("Sample Gauge")
} currentValueLabel: {
    Text("\(Int(currentValue))")
} minimumValueLabel: {
    Text("\(Int(minValue))").foregroundColor(.blue)
} maximumValueLabel: {
    Text("\(Int(maxValue))").foregroundColor(.red)
}
.gaugeStyle(SampleGaugeStyle())

ラベルしか返していないのでラベルしか表示されません。

GaugeStyleConfigurationを知る

makeBodyの引数にはconfigurationがあります。
これはConfigurationはエイリアスで中身はGaugeStyleConfigurationです。

GaugeStyleConfigurationには以下のメンバがあります。

メンバ名
labelGaugeのlabelに渡したView
currentValueLabelGaugeのcurrentValueLabelに渡したView
minimumValueLabelGaugeのminimumValueLabelに渡したView
maximumValueLabelGaugeのmaximumValueLabelに渡したView
valueGaugeのvalueを百分率に直した値(小数)
currentValue/(maxValue-minValue)

それぞれのラベルはそのままなので問題ないと思います。注意点はvalueです。
valueはGaugeのin引数に渡したClosedRangeを基準に百分率に直した値が小数で入っています。
ゲージを作る際はClosedRangeに関わらず0〜1を基準として作成して行きます。

それぞれの変数を確認してみましょう。

let currentValue = 85.0
let minValue = 0.0
let maxValue = 150.0

Gauge(value: currentValue ,in: minValue...maxValue){
    Text("Sample Gauge")
} currentValueLabel: {
    Text("\(Int(currentValue))")
} minimumValueLabel: {
    Text("\(Int(minValue))").foregroundColor(.blue)
} maximumValueLabel: {
    Text("\(Int(maxValue))").foregroundColor(.red)
}
.gaugeStyle(SampleGaugeStyle2())

////
struct SampleGaugeStyle2:GaugeStyle{
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
        configuration.currentValueLabel
        configuration.minimumValueLabel
        configuration.maximumValueLabel
        Text("\(configuration.value)")
    }
}

valueが計算後の値になっているのでcurrentValueLabelの数字と一致していません
85を渡した所が0.566667となっています。

なおClosedRangeそのものは取得できません。
必要となった場合は構造体のメンバとして追加し、
GaugeStyleを初期化する時にClosedRange渡してからgaugeStyle Modifierに渡して下さい。

////
.gaugeStyle(SampleRangeGaugeStyle(range:minValue...maxValue))


////
struct SampleRangeGaugeStyle:GaugeStyle{
    
    let range:ClosedRange<Double>
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
    }
}

簡単なGaugeStyleを実装してみる

簡単にGaugeStyleを実装してみました。
各種ラベルを配置してRectangleでゲージを表現してみました。

ゲージの現在の値をconfiguration.valueの幅のRectangleで表し、
(1 – configuration.value)の幅のRectangleで残りの部分を表して、
HStack(spacing: 0)で並べただけの簡単なGaugeStyleです。

このままでは実用性はあまりありませんが、
サンプルとしては単純で分かりやすいと思います。

struct SampleGaugeStyle:GaugeStyle{
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
        HStack{
            configuration.minimumValueLabel

            HStack(spacing: 0){
                Rectangle()
                    .frame(width: configuration.value * 100 ,height: 20)
                    .foregroundColor(.black)
                Rectangle()
                    .frame(width: (1 - configuration.value) * 100 ,height: 20)
                    .foregroundColor(.gray)
            }

            configuration.maximumValueLabel
        }
        configuration.currentValueLabel
    }
}

サンプルのゲージスタイル

先程のは少し簡単過ぎたのでもう少ししっかり作ってみました。

元々のGaugeに無い部分としては1/4でメモリを入れてみたのと、
ゲージ位置にcurrentValueLabelが追従する様にしてみました。

struct MyGaugeStyleView: View {
    let currentValue = 120.0
    let minValue = 0.0
    let maxValue = 150.0
    
    var body: some View {
        VStack {
            Gauge(value: currentValue ,in: minValue...maxValue){
                Text("temperature")
            } currentValueLabel: {
                Text("\(Int(currentValue))")
            } minimumValueLabel: {
                Text("\(Int(minValue))")
                    .foregroundColor(.blue)
            } maximumValueLabel: {
                Text("\(Int(maxValue))")
                    .foregroundColor(.red)
            }
            .gaugeStyle(MyGaugeStyle())
            .frame(height: 500)
        }.padding()
    }
}

struct MyGaugeStyle:GaugeStyle{
    
    let gaugeWidth:CGFloat = 30
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
        configuration.maximumValueLabel
        
        GeometryReader{ geometry in
            HStack {
                Spacer()
                ZStack {
                    gaugePath(configuration.value ,width: gaugeWidth ,height: geometry.size.height)
                        .fill(Gradient(colors: [.red, .yellow, .blue]), style: FillStyle(eoFill: true))
                        .background(Color.gray)
                        .cornerRadius(10)
                        .frame(width: gaugeWidth, height: geometry.size.height)
                    
                    Rectangle()
                        .frame(width: gaugeWidth,height: 1)
                        .offset(CGSize(width: 0, height: geometry.size.height / -4 ))
                    
                    Rectangle()
                        .frame(width: gaugeWidth,height: 1)
                        .offset(CGSize(width: 0, height: 0 ))
                    
                    Rectangle()
                        .frame(width: gaugeWidth,height: 1)
                        .offset(CGSize(width: 0, height: geometry.size.height / 4 ))
                    
                    VStack{
                        configuration.currentValueLabel
                            .offset(CGSize(width: gaugeWidth, height: (configuration.value * geometry.size.height * -1) + geometry.size.height/2 ))
                    }
                }
                Spacer()
            }
        }
        
        configuration.minimumValueLabel
    }
    
    func gaugePath(_ value:Double ,width:CGFloat ,height:CGFloat) -> Path{
        var path = Path()
        path.addRect(CGRect(x:0, y:0, width: width,height: height))
        path.addRect(CGRect(x:0, y:0, width: width,height: height))//fillが偶数のみ塗られる為、数合わせのダミー
        path.addRect(CGRect(x:0, y:(1 - value) * height, width: gaugeWidth,height: value * height))
        return path
    }
}

図形の描画は本筋でないのでざっくり簡単にだけ説明します。

GeometryReader

GeometryReaderを使っています。
これはゲージの高さををViewの高さに合わせて変更する為に使用しました。
GeometryReaderで取得した高さにconfiguration.valueをかけて現在のゲージの長さを指定しています。
今回は幅は固定値にしています。

currentValueLabelの追従も上記同様にGeometryReaderを使っています。
あとはoffsetでゲージ上にならない様に右にずらしています。

なおGeometryReaderの中は自動的に右上揃えになるのでHStackとSpacerで中心に移動させています。

Path

ゲージ自体はPathで描画しました。色はfillで塗っています。
fillを全体にかけつつ現在値のゲージのみに適用する事で上手くグラデーションさせています。
現在値のみにfillをかけると長さに関わらす最後までグラデーションするので注意です。

Pathは長方形を3つ重ねて描画しています。背景とダミーと現在値です。
上記のグラデーションの都合でFillStyleを使うのですが、
FillStyle(eoFill: true)で奇数枚重なった箇所のみ色が付きます。
その為、背景をダミーと重ねて偶数にして、その上に重なった現在値のみ奇数にします。

最後に

GaugeはGaugeStyleを独自に定義する事で自由度が増します。
複数箇所で使う場合などに便利なので覚えておきましょう。

また、SwiftUIにはこの様に〜Styleを定義できるようになっているViewは複数あります。
似たような構成なので1つ覚えると他のものを使うときにも役に立つと思います。

Gaugeの基本についての記事はこちらです。

コメント

  1. […] […]

タイトルとURLをコピーしました