Skip to content

Instantly share code, notes, and snippets.

@S2dentik
Created April 29, 2020 18:39
Show Gist options
  • Save S2dentik/843b388d2611d022cdf1494796914a70 to your computer and use it in GitHub Desktop.
Save S2dentik/843b388d2611d022cdf1494796914a70 to your computer and use it in GitHub Desktop.
Simple Clock built using SwiftUI and Combine
import SwiftUI
import Combine
import Foundation
import PlaygroundSupport
// Make it animatable
struct ClockModel: VectorArithmetic {
static func - (lhs: ClockModel, rhs: ClockModel) -> ClockModel {
ClockModel(seconds: lhs.seconds - rhs.seconds)
}
static func + (lhs: ClockModel, rhs: ClockModel) -> ClockModel {
ClockModel(seconds: lhs.seconds + rhs.seconds)
}
mutating func scale(by rhs: Double) {
seconds *= rhs
}
var magnitudeSquared: Double { seconds * seconds }
static var zero = ClockModel(seconds: 0)
var seconds: TimeInterval
var hours: Double { seconds / 3600 }
var minutes: Double { (seconds - Double(Int(hours) * 3600)) / 60 }
}
// Old habits
extension CGRect {
var center: CGPoint {
CGPoint(x: midX, y: midY)
}
}
// Draw a clock, animated on its model
struct Clock: Shape {
var model: ClockModel
var animatableData: ClockModel {
get { model }
set { model = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let length = rect.width / 2
path.addArc(center: rect.center, radius: length, startAngle: .zero, endAngle: .init(degrees: 360), clockwise: true)
path.move(to: rect.center)
let hoursAngle = CGFloat.pi / 2 - .pi * 2 * CGFloat(model.hours) / 12
path.addLine(to: CGPoint(x: rect.center.x + cos(hoursAngle) * length * 0.5,
y: rect.center.y - sin(hoursAngle) * length * 0.5))
path.move(to: rect.center)
let minutesAngle = CGFloat.pi / 2 - .pi * 2 * CGFloat(model.minutes) / 60
path.addLine(to: CGPoint(x: rect.center.x + cos(minutesAngle) * length * 0.7,
y: rect.center.y - sin(minutesAngle) * length * 0.7))
path.move(to: rect.center)
let secondsAngle = CGFloat.pi / 2 - .pi * 2 * CGFloat(model.seconds.remainder(dividingBy: 60)) / 60
path.addLine(to: CGPoint(x: rect.center.x + cos(secondsAngle) * length * 0.9,
y: rect.center.y - sin(secondsAngle) * length * 0.9))
return path
}
}
// Play a bit with combine
final class CurrentTime: ObservableObject {
@Published var seconds: TimeInterval = CurrentTime.currentSecond(date: Date())
private let timer = Timer.publish(every: 1, on: .main, in: .default).autoconnect()
private var store = Set<AnyCancellable>()
init() {
timer.map(Self.currentSecond).assign(to: \.seconds, on: self).store(in: &store)
}
private static func currentSecond(date: Date) -> TimeInterval {
let components = Calendar.current.dateComponents([.year, .month, .day], from: date)
let referenceDate = Calendar.current.date(from: DateComponents(year: components.year!, month: components.month!, day: components.day!))!
return Date().timeIntervalSince(referenceDate)
}
}
// Display
struct ContentView: View {
@ObservedObject var time = CurrentTime()
var body: some View {
return Clock(model: .init(seconds: time.seconds))
.stroke(RadialGradient(gradient: Gradient(colors: [.white, .black]), center: .center, startRadius: 0, endRadius: 150), lineWidth: 3)
.shadow(radius: 2, x: 5, y: 5)
.animation(.linear(duration: 1))
}
}
let view = ContentView()
PlaygroundPage.current.setLiveView(view)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment