I would like to create a clock style with the new GaugeStyle since iOS 16. You can see my code and the preview in this post. I would like the draw the 3,6,9,12 lines (bold black). The color line show the actual hours (this part works well).
Gauge(value: Double(hour.formatted(.number)) ?? 0, in: 0...24) {
Image(systemName: "gauge.medium")
.font(.system(size: 50.0))
} currentValueLabel: {
Text(Image(systemName: weatherModel.symbol ?? "xmark.icloud"))
}
.gaugeStyle(clockGaugeStyle())
AND the struct code
struct clockGaugeStyle: GaugeStyle {
private var purpleGradient = LinearGradient(gradient: Gradient(colors: [ Color(red: 207/255, green: 150/255, blue: 207/255), Color(red: 107/255, green: 116/255, blue: 179/255) ]), startPoint: .trailing, endPoint: .leading)
func makeBody(configuration: Configuration) -> some View {
ZStack {
Circle()
.trim(from: 0, to: 0.75 * configuration.value)
.stroke(purpleGradient, lineWidth: 20)
.rotationEffect(.degrees(135))
Circle()
.trim(from: 0, to: 0.75)
.stroke(Color.black, style: StrokeStyle(lineWidth: 10, lineCap: .butt, lineJoin: .round, dash: [1, 96], dashPhase: 0.0))
.rotationEffect(.degrees(360))
VStack {
configuration.currentValueLabel
.font(.system(size: 80, weight: .bold, design: .rounded))
.foregroundColor(.gray)
}
}
.frame(width: 300, height: 300)
}
}
CodePudding user response:
I understand you want to draw hour marks and 3,6,9,12 hour prominent marks over the gauge.
This would be my solution. You won't need the GeometryReader
if the size is always fixed.
struct clockGaugeStyle: GaugeStyle {
private var purpleGradient = LinearGradient(gradient: Gradient(colors: [ Color(red: 207/255, green: 150/255, blue: 207/255), Color(red: 107/255, green: 116/255, blue: 179/255) ]), startPoint: .trailing, endPoint: .leading)
func makeBody(configuration: Configuration) -> some View {
GeometryReader { geo in
ZStack {
Circle()
.trim(from: 0, to: 0.75 * configuration.value)
.stroke(purpleGradient, lineWidth: 20)
.rotationEffect(.degrees(135))
// hours
ForEach(0..<12) { hour in
Color.black.opacity(0.5)
.frame(width: 20, height: 1)
.offset(x: geo.size.width / 2 )
.rotationEffect(.degrees(Double(hour * 30)))
}
// 3,6,9,12
ForEach(0..<4) { hour in
Color.black
.frame(width: 20, height: 4)
.offset(x: geo.size.width / 2 )
.rotationEffect(.degrees(Double(hour * 3 * 30)))
}
configuration.currentValueLabel
.font(.system(size: 80, weight: .bold, design: .rounded))
.foregroundColor(.gray)
}
}
.frame(width: 300, height: 300)
}
}
CodePudding user response:
I'd use a Canvas
to draw the gauge. It's more code, but I find it easier to understand. Here's what mine looks like:
Even if you don't use a Canvas
, you should consider using an AngularGradient
instead of a LinearGradient
. Compare these two gauges:
The one on the left uses an angular gradient. The one on the right uses a linear gradient. Notice that on the left, the color smoothly changes as it goes around from the start to the end of the arc. On the right, notice that most of the left side of the arc looks all pink, and most of the right side of the arc looks all indigo. The linear gradient is only noticeable between about 10:30 and 1:30.
Here's my code (using the angular gradient):
struct ContentView: View {
@State var hour = 12
var body: some View {
VStack {
Spacer()
Gauge(value: Double(hour), in: 0...24) {
Image(systemName: "gauge.medium")
.font(.system(size: 50.0))
} currentValueLabel: {
Image(systemName: "xmark.icloud")
}
.gaugeStyle(.clock)
.animation(.easeOut, value: hour)
Slider(
value: .init(
get: { Double(hour) },
set: { hour = Int($0.rounded()) }
),
in: 0.0 ... 24.0
)
}
.padding()
}
}
extension GaugeStyle where Self == ClockGaugeStyle {
static var clock: Self { .init() }
}
struct ClockGaugeStyle: GaugeStyle {
func makeBody(configuration: Configuration) -> some View {
ZStack {
configuration.currentValueLabel
.font(.system(
size: 80,
weight: .bold,
design: .rounded))
.foregroundColor(.gray)
GaugeGraphic(animatableData: configuration.value)
}
.frame(width: 300, height: 300)
}
private struct GaugeGraphic: View, Animatable {
var animatableData: Double
var gradient: Gradient {
.init(stops: [
.init(color: Color(#colorLiteral(red: 0.8117647059, green: 0.5882352941, blue: 0.8117647059, alpha: 1)), location: 0),
.init(color: Color(#colorLiteral(red: 0.8117647059, green: 0.5882352941, blue: 0.8117647059, alpha: 1)), location: 0.125),
.init(color: Color(#colorLiteral(red: 0.4196078431, green: 0.4549019608, blue: 0.7019607843, alpha: 1)), location: 0.875),
.init(color: Color(#colorLiteral(red: 0.4196078431, green: 0.4549019608, blue: 0.7019607843, alpha: 1)), location: 1.0),
])
}
var body: some View {
Canvas { gc, size in
let center = CGPoint(x: 0.5 * size.width, y: 0.5 * size.height)
gc.translateBy(x: center.x, y: center.y)
let arcWidth = size.width * 0.08
let arc = Path {
let baseDegrees = 135.0
$0.addArc(
center: .zero, radius: 0.5 * (size.width - arcWidth),
startAngle: .degrees(baseDegrees),
// max with 0.0001 so it draws a dot if animatableData == 0
endAngle: .degrees(baseDegrees 270 * max(0.0001, animatableData)),
clockwise: false
)
}
let shading = GraphicsContext.Shading.conicGradient(
gradient, center: .zero,
angle: .degrees(90)
)
gc.stroke(
arc, with: shading,
style: .init(lineWidth: arcWidth, lineCap: .round)
)
for hour in 0 ..< 12 {
let tick = Path {
$0.move(to: .init(x: 0.5 * size.width - 7, y: 0))
$0.addLine(to: .init(x: 0.5 * size.width - arcWidth 7, y: 0))
}.applying(.init(rotationAngle: Double(hour) * .pi / 6))
gc.stroke(
tick,
with: .color(.black),
style: .init(
lineWidth: hour.isMultiple(of: 3) ? 3 : 1,
lineCap: .round
)
)
}
}
}
}
}
The only difference for the linear gradient is how I initialize the Shading
:
let shading = GraphicsContext.Shading.linearGradient(
gradient,
startPoint: .init(x: -0.5 * size.width, y: 0),
endPoint: .init(x: 0.5 * size.width, y: 0)
)