Created
November 6, 2023 11:33
-
-
Save UnderscoreDavidSmith/aba05adb41a95d0bce1bf74bc3e7ca7e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContentView.swift | |
// ClockRotationBug | |
// | |
// Created by David Smith on 11/6/23. | |
// | |
import SwiftUI | |
struct ContentView: View { | |
@State var date:Date = Date.dateOn(year: 2023, month: 12, day: 31, hour: 23, minute: 58, second: 0) | |
var body: some View { | |
VStack { | |
rotationBugEntryView(date: date) | |
.frame(width:200, height:200) | |
HStack { | |
Button(action: { | |
withAnimation { | |
self.date = date.addingTimeInterval(-60) | |
} | |
}, label: { | |
Text("Last") | |
}) | |
Button(action: { | |
withAnimation { | |
self.date = date.addingTimeInterval(60) | |
} | |
}, label: { | |
Text("Next") | |
}) | |
} | |
} | |
} | |
} | |
#Preview { | |
ContentView() | |
} | |
struct rotationBugEntryView : View { | |
var date: Date | |
var hourAngle:Angle { | |
let anchor = Date.dateOn(year: 2023, month: 1, day: 1, hour: 0, minute: 0, second: 0) | |
let startOfDay = Calendar.current.startOfDay(for: date) | |
let daysBetween = Calendar.current.dateComponents([.day], from: anchor, to: startOfDay) | |
guard let numberOfDays = daysBetween.day else { | |
return .zero | |
} | |
let dayStartAngle = Double(numberOfDays) * (360.0 * 2.0) | |
let components = Calendar.current.dateComponents ([.hour, .minute], from: date) | |
if let hour = components.hour, let minute = components.minute { | |
let percentHourOfDay = ((Double(hour) + Double(minute) / 60.0)) / 24.0 | |
let hourAngleInDay = (percentHourOfDay * (360.0 * 2.0)) | |
return Angle.degrees(dayStartAngle + hourAngleInDay ) | |
} | |
return .zero | |
} | |
var minuteAngle:Angle { | |
let anchor = Date.dateOn(year: 2023, month: 1, day: 1, hour: 0, minute: 0, second: 0) | |
let startOfDay = Calendar.current.startOfDay(for: date) | |
let daysBetween = Calendar.current.dateComponents([.day], from: anchor, to: startOfDay) | |
guard let numberOfDays = daysBetween.day else { | |
return .zero | |
} | |
let dayStartAngle = Double(numberOfDays) * (360.0 * 24.0) | |
let components = Calendar.current.dateComponents ([.hour, .minute], from: date) | |
if let hour = components.hour, let minute = components.minute { | |
let percentMinuteInDay = Double(hour * 60 + minute) / (24.0 * 60.0) | |
let minuteAngleInDay = percentMinuteInDay * (360.0 * 24.0) | |
return Angle.degrees(dayStartAngle + minuteAngleInDay) | |
} | |
return Angle.zero | |
} | |
var body: some View { | |
VStack { | |
GeometryReader { proxy in | |
ZStack { | |
Circle() | |
.fill(Color.gray) | |
ForEach(0..<12) { hour in | |
VStack(spacing:2) { | |
Rectangle() | |
.fill(.white) | |
.frame(width: 2, height: 5) | |
Text("\(hour == 0 ? 12 : hour)") | |
.font(.system(size: 8)) | |
.foregroundStyle(.white) | |
.rotationEffect(Angle(degrees:-360.0 * ( Double(hour) / 12.0))) | |
.frame(width: 10, height: 5) | |
Spacer() | |
} | |
.rotationEffect(Angle(degrees:360.0 * ( Double(hour) / 12.0))) | |
} | |
ClockHand(lengthPercent: 1.0) | |
.stroke(.black, style: StrokeStyle(lineWidth: 7, lineCap: .round)) | |
.shadow(radius: 1) | |
.rotationEffect(minuteAngle) | |
ClockHand(lengthPercent: 0.7) | |
.stroke(.white, style: StrokeStyle(lineWidth: 7, lineCap: .round)) | |
.shadow(radius: 1) | |
.rotationEffect(hourAngle) | |
} | |
} | |
Text(date, style: .time) | |
Text(formatDate()) | |
} | |
} | |
func formatDate() -> String { | |
let formatter = DateFormatter() | |
formatter.timeStyle = .none | |
formatter.dateStyle = .medium | |
return formatter.string(from: date) | |
} | |
} | |
struct ClockHand:Shape { | |
let lengthPercent:Double | |
func path(in rect: CGRect) -> Path { | |
var path = Path() | |
path.move(to: CGPoint(x: rect.midX, y: rect.midY)) | |
path.addLine(to: CGPoint(x: rect.midX, y: 3.5 + rect.height * (0.5 * (1.0 - lengthPercent)))) | |
return path | |
} | |
} | |
extension Date { | |
static func dateOn(year:Int, month:Int, day:Int, hour:Int, minute:Int, second:Int) -> Date { | |
var components = DateComponents() | |
components.year = year | |
components.month = month | |
components.day = day | |
components.minute = minute | |
components.hour = hour | |
components.second = second | |
return Calendar.current.date(from: components)! | |
} | |
} | |
extension Date { | |
static func dateSince(days:Int, hour:Int, minute:Int, second:Int) -> Date { | |
let anchor = Date.dateOn(year: 2023, month: 1, day: 1, hour: 0, minute: 0, second: 0) | |
var sum:Double = 0.0 | |
sum += Double(days) * (24.0 * 60 * 60) | |
sum += Double(hour) * (60 * 60) | |
sum += Double(minute) * (60) | |
sum += Double(second) | |
return anchor.addingTimeInterval(sum) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment