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()
}
}
}
いい感じになりました。これで画面サイズに依存せずにゲージの大きさをピッタリ収めることができます。
アニメーション効果
通常どおりvalueに値を代入する箇所をwithAnimationで囲むだけでOKでした。(ウジェットではアニメーション効果が封印されているのでアプリ本体で使おう)
.onAppear {
withAnimation(.easeInOut(duration: 1)) {
value = inputValue
}
}
.onChange(of: inputValue) { newValue in
withAnimation(.easeInOut(duration: 1)) {
value = newValue.rounded(.toNearestOrAwayFromZero)
}
}
完成イメージ
サンプルコード
GitHubにプロジェクト一式を置きましたので良かったら参考にしてください。
https://github.com/yuppejp/GaugeSample-ios
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