【iOS16】Gaugeのサイズを変更するには？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/d2b134da-07af-bb8e-5f14-b6e963ed5619.png)
iOS16で使えるようになったSwiftUIのGaugeのおかげで自前でゲージ型のプログレスを書かなくて良くなりそうです。さっそく使ってみたところ、円形のゲージのサイズ変更で意外と手間取ったのでその時のメモです。

# ゲージの大きさが変わらない
モディファイアーがaccessoryCircularになっていることからWidgetkitのウィジェットで使用することを想定しているようで、ゲージが固定サイズで.frameを使ったサイズ変更が効きませんでした。

| 円形のゲージが小さい  |  こうしたい  |
| ---- | ---- |
|  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/dc1dfc46-3ba0-fe57-da78-16741f746277.png" width="300">  |  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/b82798eb-b975-8f01-891c-e2b950cbb031.png" width="300">  |

## .frameが効かない
.frameでwidthとheightを指定しても背景の余白の大きさが変わるだけでゲージの大きさは固定されたままです。

```swift
    Text("accessoryCircularCapacity")
        .font(.caption)
    Gauge(value: value, in: 0...100) {
        Text(0.5, format: .percent)
    }
    .gaugeStyle(.accessoryCircularCapacity)
    .tint(.green)
    .frame(width: 300, height: 300)
    .background(Color.gray.opacity(0.3))
```
小さいまま
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/27e834e3-0e56-6663-0fd5-fae05e20ac1c.png" width="300">


## .scaleEffectが効いた
.scaleEffectで倍率を指定することでゲージが大きくなりました。ただ、この倍率を描画領域に応じて変更する必要があるので汎用性が低いです。ここをなんとかしたいところです。

```swift
    Text("accessoryCircularCapacity")
        .font(.caption)
    Gauge(value: value, in: 0...100) {
        Text(value / 100, format: .percent)
    }
    .gaugeStyle(.accessoryCircularCapacity)
    .tint(.green)
    .scaleEffect(4.0) // <-- これ
    .frame(width: 300, height: 300)
    .background(Color.gray.opacity(0.3))
```
大きくなった
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/a04d96da-d774-cd88-6689-695cdfbc8c9d.png" width="300">

## スケールの倍率を計算で求める
GeometryReaderを多段で使用して、外側の描画領域と内側のGaugeの大きさの比率を求めて、これをscaleEffectに反映します。また、このままだと以前の位置のままになってしまうので、paddingで位置を調整します。

```swift
struct ScaleGaugeSample: View {
    var value = 50.0
    @State var scale = 1.0
    @State var padding = 0.0
    
    var body: some View {
        GeometryReader{ geometry in
            VStack {
                Text("accessoryCircularCapacity")
                    .font(.caption)
                Gauge(value: value, in: 0...100) {
                    Text(value / 100, format: .percent)
                }
                .gaugeStyle(.accessoryCircularCapacity)
                .scaleEffect(scale)
                .tint(.green)
                .background(GeometryReader{ inner -> Text in
                    DispatchQueue.main.async {
                        let diameter = geometry.size.width * 0.8
                        scale = diameter / inner.size.width
                        padding = (diameter - inner.size.width) / 2
                    }
                    return Text("")
                })
                .padding(padding)
                .background(Color.gray)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity) // GeometryReaderを使用するとセンタリングされない問題の対処
            .padding()
        }
    }
}
```
いい感じになりました。これで画面サイズに依存せずにゲージの大きさをピッタリ収めることができます。
![スクリーンショット 2022-10-09 11.23.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/8fa709ad-3cce-ea99-8ff3-9e493221cb55.png)

# アニメーション効果
通常どおりvalueに値を代入する箇所をwithAnimationで囲むだけでOKでした。（ウジェットではアニメーション効果が封印されているのでアプリ本体で使おう）

```swift
    .onAppear {
        withAnimation(.easeInOut(duration: 1)) {
            value = inputValue
        }
    }
    .onChange(of: inputValue) { newValue in
        withAnimation(.easeInOut(duration: 1)) {
            value = newValue.rounded(.toNearestOrAwayFromZero)
        }
    }
```

# 完成イメージ
![gauge.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/2dc04bc9-57d8-e58b-3d05-76fe425c73d6.gif)

# サンプルコード
GitHubにプロジェクト一式を置きましたので良かったら参考にしてください。
https://github.com/yuppejp/GaugeSample-ios

<details>
<summary>ContentView.swift</summary>

```swift:ContentView.swift
import SwiftUI
struct GaugeSample2: View {
    @State var value = 0.0
    @State var inputValue = 50.0
    @State var scale = 1.0
    @State var padding = 0.0
    
    var body: some View {
        GeometryReader{ geometry in
            VStack {
                HStack {
                    VStack {
                        Text("accessoryCircularCapacity")
                            .font(.caption)
                        Gauge(value: value, in: 0...100) {
                            Text(value / 100, format: .percent)
                        }
                        .gaugeStyle(.accessoryCircularCapacity)
                        .scaleEffect(scale)
                        .tint(.green)
                        .background(GeometryReader{ inner -> Text in
                            DispatchQueue.main.async {
                                let diameter = geometry.size.width / 2.5
                                scale = diameter / inner.size.width
                                padding = (diameter - inner.size.width) / 2
                                
                                print("***** geometry: \(geometry.size.width), \(geometry.size.height)")
                                print("***** inner   : \(inner.size.width), \(inner.size.height)")
                                print("***** scale   : \(scale)")
                            }
                            return Text("")
                        })
                        .padding(padding)
                    }
                    VStack {
                        Text("accessoryCircular")
                            .font(.caption)
                        Gauge(value: value, in: 0...100) {
                        } currentValueLabel: {
                            Text(value / 100, format: .percent)
                        } minimumValueLabel: {
                            Text("0")
                        } maximumValueLabel: {
                            Text("100")
                        }
                        .gaugeStyle(.accessoryCircular)
                        .scaleEffect(scale)
                        .tint(Gradient(colors: [.red, .yellow, .green, .green, .green]))
                        .padding(padding)
                    }
                }
                VStack {
                    Text("accessoryLinear")
                    Gauge(value: value, in: 0...100) {
                    }
                    .gaugeStyle(.accessoryLinear)
                    .tint(.blue)
                    
                    Text("accessoryLinearCapacity")
                    Gauge(value: value, in: 0...100) {
                    }
                    .gaugeStyle(.accessoryLinearCapacity)
                    .tint(.cyan)
                    Text("linearCapacity")
                    
                    Gauge(value: value, in: 0...100) {
                    }
                    .gaugeStyle(.linearCapacity)
                    .tint(.orange)
                }
                Spacer()
                Slider(value: $inputValue, in: 0...100)
                Text(String(value))
            }
            .onAppear {
                withAnimation(.easeInOut(duration: 1)) {
                    value = inputValue
                }
            }
            .onChange(of: inputValue) { newValue in
                withAnimation(.easeInOut(duration: 1)) {
                    value = newValue.rounded(.toNearestOrAwayFromZero)
                }
            }
            .padding()
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            GaugeSample2()
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

```

</details>

## 環境
- Xcode 14.0.1
- iOS 16.0.2


# 参考サイト
- [Gauge - Apple Developer](https://developer.apple.com/documentation/swiftui/gauge)
- [SwiftUI Gauge — A New Way of Showing Progress](https://betterprogramming.pub/swiftui-gauge-new-way-of-progress-showing-25fb58fb11e3)