Skip to content

Instantly share code, notes, and snippets.

@waynedahlberg
Last active November 4, 2024 16:49
Show Gist options
  • Select an option

  • Save waynedahlberg/bc231bf0ff6cf09b19e4252a2107d1dc to your computer and use it in GitHub Desktop.

Select an option

Save waynedahlberg/bc231bf0ff6cf09b19e4252a2107d1dc to your computer and use it in GitHub Desktop.
Analog Clock with sweeping second hand
//
// ContentView.swift
// Analog Clock with sweeping second hand
//
// Created by Wayne Dahlberg on 11/3/24.
//
import SwiftUI
struct ContentView: View {
@State private var currentTime = Date()
let timer = Timer.publish(every: 0.125, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Color(.init(white: 0.925, alpha: 1.0))
.edgesIgnoringSafeArea(.all)
ClockFace()
.onReceive(timer) { input in
currentTime = input
}
.environmentObject(TimeState(date: currentTime))
}
}
}
class TimeState: ObservableObject {
@Published var date: Date
init(date: Date) {
self.date = date
}
}
struct ClockFace: View {
@EnvironmentObject var timeState: TimeState
var body: some View {
GeometryReader { geometry in
let diameter = min(geometry.size.width, geometry.size.height)
let radius = diameter / 2
let center = CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)
ZStack {
// Clock face background
Circle()
.fill(.white)
.shadow(color: .black.opacity(0.15), radius: 8, x: 0, y: 8)
.padding(16)
// Minute markers
ForEach(0..<60) { minute in
let angle = Double(minute) * .pi * 2 / 60
let markerStart = CGPoint(
x: center.x + CGFloat(sin(angle)) * (radius * 0.85),
y: center.y - CGFloat(cos(angle)) * (radius * 0.85)
)
let markerEnd = CGPoint(
x: center.x + CGFloat(sin(angle)) * (radius * 0.9),
y: center.y - CGFloat(cos(angle)) * (radius * 0.9)
)
Path { path in
path.move(to: markerStart)
path.addLine(to: markerEnd)
}
.stroke(Color.black, style: StrokeStyle(lineWidth: 1, lineCap: .round))
}
// Hour markers
ForEach(0..<12) { hour in
let angle = Double(hour) * .pi * 2 / 12
let markerStart = CGPoint(
x: center.x + CGFloat(sin(angle)) * (radius * 0.8),
y: center.y - CGFloat(cos(angle)) * (radius * 0.8)
)
let markerEnd = CGPoint(
x: center.x + CGFloat(sin(angle)) * (radius * 0.9),
y: center.y - CGFloat(cos(angle)) * (radius * 0.9)
)
Path { path in
path.move(to: markerStart)
path.addLine(to: markerEnd)
}
.stroke(Color.black, lineWidth: 2)
}
// Clock hands
ClockHands(center: center, radius: radius)
}
.frame(width: diameter, height: diameter)
.position(center)
}
.aspectRatio(1, contentMode: .fit)
}
}
struct ClockHands: View {
@EnvironmentObject var timeState: TimeState
let center: CGPoint
let radius: CGFloat
var calendar: Calendar {
var calendar = Calendar.current
calendar.locale = Locale(identifier: "en_US_POSIX")
return calendar
}
var hourAngle: Double {
let hour = Double(calendar.component(.hour, from: timeState.date))
let minute = Double(calendar.component(.minute, from: timeState.date))
return (hour + minute / 60) * .pi * 2 / 12
}
var minuteAngle: Double {
let minute = Double(calendar.component(.minute, from: timeState.date))
let second = Double(calendar.component(.second, from: timeState.date))
let millisecond = Double(calendar.component(.nanosecond, from: timeState.date)) / 1_000_000_000
return (minute + (second + millisecond) / 60) * .pi * 2 / 60
}
var secondAngle: Double {
let second = Double(calendar.component(.second, from: timeState.date))
let millisecond = Double(calendar.component(.nanosecond, from: timeState.date)) / 1_000_000_000
return (second + millisecond) * .pi * 2 / 60
}
var body: some View {
ZStack {
// Hour hand
Path { path in
path.move(to: center)
path.addLine(to: CGPoint(
x: center.x + CGFloat(sin(hourAngle)) * (radius * 0.5),
y: center.y - CGFloat(cos(hourAngle)) * (radius * 0.5)
))
}
.stroke(style: StrokeStyle(lineWidth: radius * 0.04, lineCap: .round))
.shadow(color: .black.opacity(0.15), radius: 1, x: 0, y: 3)
// Minute hand
Path { path in
path.move(to: center)
path.addLine(to: CGPoint(
x: center.x + CGFloat(sin(minuteAngle)) * (radius * 0.7),
y: center.y - CGFloat(cos(minuteAngle)) * (radius * 0.7)
))
}
.stroke(style: StrokeStyle(lineWidth: radius * 0.025, lineCap: .round))
.shadow(color: .black.opacity(0.15), radius: 1, x: 0, y: 3)
// Second hand
Path { path in
path.move(to: CGPoint(
x: center.x - CGFloat(sin(secondAngle)) * (radius * 0.2),
y: center.y + CGFloat(cos(secondAngle)) * (radius * 0.2)
))
path.addLine(to: CGPoint(
x: center.x + CGFloat(sin(secondAngle)) * (radius * 0.8),
y: center.y - CGFloat(cos(secondAngle)) * (radius * 0.8)
))
}
.stroke(Color.red, style: StrokeStyle(lineWidth: radius * 0.01, lineCap: .round))
.shadow(color: .black.opacity(0.15), radius: 1, x: 0, y: 3)
// Center pin
Circle()
.fill(Color.red.gradient)
.frame(width: radius * 0.05, height: radius * 0.05)
.shadow(color: .black.opacity(0.15), radius: 1, x: 0, y: 3)
.position(center)
}
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment