This nested ring UI works well but how can I code it so it scales whether its parent is very small or very large?
import SwiftUI
struct CustomGaugeStyleView: View {
@State private var innerRingFill = 6.5
var body: some View {
Gauge(value: innerRingFill, in: 0...10) {
Image(systemName: "gauge.medium")
.font(.system(size: 50.0))
} currentValueLabel: {
Text("\(innerRingFill.formatted(.number))")
}
.gaugeStyle(twoRingGaugeStyle(outerRingMin: 5.5, outerRingMax: 7.5))
}
}
struct CustomGaugeStyleView_Previews: PreviewProvider {
static var previews: some View {
CustomGaugeStyleView()
}
}
struct twoRingGaugeStyle: GaugeStyle {
var outerRingMin: Double
var outerRingMax: Double
func makeBody(configuration: Configuration) -> some View {
GeometryReader { geometry in
ZStack {
Circle()
.stroke(Color(.lightGray).opacity(0.2), style: StrokeStyle(lineWidth: 20))
.frame(height: geometry.size.height * 0.70)
Circle()
.trim(from: 0, to: 0.75 * configuration.value)
.stroke(Color.orange.gradient, style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(270))
.frame(height: geometry.size.height * 0.70)
Circle()
.trim(from: outerRingMin / 10, to: outerRingMax / 10)
.stroke(Color.green.gradient, style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(270))
.frame(height: geometry.size.height * 0.82)
}
.padding()
}
.aspectRatio(contentMode: .fit)
}
}
the first image is the view without any frame size, the second view is with adding .frame(height: 100)
to the Gauge.
As @loremipsum mentioned in the comments, if you want this UI to scale for any screen size, then your lineWidth
needs to be a percentage.
Here is an example implementation:
struct CustomGuageStyleView: View {
@State private var innerRingFill = 6.5
var body: some View {
Gauge(value: innerRingFill, in: 0...10) {
Image(systemName: "gauge.medium")
.font(.system(size: 50.0))
} currentValueLabel: {
Text("\(innerRingFill.formatted(.number))")
}
.gaugeStyle(twoRingGaugeStyle(outerRingMin: 5.5, outerRingMax: 7.5))
}
}
struct twoRingGaugeStyle: GaugeStyle {
var outerRingMin: Double
var outerRingMax: Double
//This is not strictly necessary but it gives you an option
var multiplierAmount: Double = 0.045
func makeBody(configuration: Configuration) -> some View {
GeometryReader { geometry in
ZStack {
Circle()
.stroke(Color(.lightGray).opacity(0.2), style: StrokeStyle(lineWidth: geometry.size.width * multiplierAmount)) ///<<<--- HERE!! We are now making this variable.
.frame(height: geometry.size.height * 0.70)
Circle()
.trim(from: 0, to: 0.75 * configuration.value)
.stroke(Color.orange.gradient, style: StrokeStyle(lineWidth: geometry.size.width * multiplierAmount, lineCap: .round, lineJoin: .round)) ///<<<--- HERE too.
.rotationEffect(.degrees(270))
.frame(height: geometry.size.height * 0.70)
Circle()
.trim(from: outerRingMin / 10, to: outerRingMax / 10)
.stroke(Color.green.gradient, style: StrokeStyle(lineWidth: geometry.size.width * multiplierAmount, lineCap: .round, lineJoin: .round)) ///<<<--- HERE!! We are now making this variable.
.rotationEffect(.degrees(270))
.frame(height: geometry.size.height * 0.78) // <<<--- Here, I changed the value to 0.75 - this will make the rings slightly closer together, which works better with the new scaling.
//- NOTE: I might add a `minHeight` above, or the circle will end up eventually having not enough of a change between the inner value to appear separated.
}
.padding()
}
.aspectRatio(contentMode: .fit)
}
}
Explanation
There are only a few changes to the code here, and all of them are inside the twoRingGuageStyle
.
lineWidth
inside the StrokeStyle
, I changed the value from 20
to geometry.size.width * multiplierAmount
. This makes the line width also scale with the GeometryReader
.multiplierAmount
, to the top of the twoRingGuageStyle
. This allows you to optionally configure the Circle
width. The default value is 0.045
.frame
on the outer Circle
at height: geometry.size.height * 0.82
, as I mentioned in the comment on that line,I might add a minHeight
above, or the circle will end up eventually having not enough of a change between the inner value to appear separated.
Otherwise, at very small screen sizes, your circles will appear to not be spaced out enough.
Screenshots
Note
This code was tested with Xcode 14.2 and macOS 13.1. It may require minute adjustments of the values for the UI to be of the exact look needed.