Home > database >  swiftUI Clock(Gauge style)
swiftUI Clock(Gauge style)

Time:01-16

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

Sample

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.

enter image description here

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:

A gauge. It has tick marks at the standard clock hour positions, with the ticks for positions 3, 6, 9, and 12 thicker than the others. It has a color arc that goes from about the 7:30 position clockwise around to about the 4:30 position. The color at 7:30 is pink. The color at 4:30 is indigo.

Even if you don't use a Canvas, you should consider using an AngularGradient instead of a LinearGradient. Compare these two gauges:

Two gauges side by side. The gauge on the left uses a conic gradient. The gauge on the right uses a left-to-right linear gradient.

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