6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

image.png
iOS16で使えるようになったSwiftUIのGaugeのおかげで自前でゲージ型のプログレスを書かなくて良くなりそうです。さっそく使ってみたところ、円形のゲージのサイズ変更で意外と手間取ったのでその時のメモです。

ゲージの大きさが変わらない

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

円形のゲージが小さい こうしたい

.frameが効かない

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

    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))

小さいまま

.scaleEffectが効いた

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

    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))

大きくなった

スケールの倍率を計算で求める

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

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

アニメーション効果

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

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

完成イメージ

gauge.gif

サンプルコード

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

ContentView.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()
    }
}

環境

  • Xcode 14.0.1
  • iOS 16.0.2

参考サイト

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?