Вынесена реализация виджета в отдельный target с корректным embedding и Info.plist, чтобы сборка и запуск работали стабильно на устройстве, а также улучшена адаптивная верстка меток и фонового контейнера для соответствия требованиям WidgetKit. Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
6.3 KiB
Swift
Executable File
135 lines
6.3 KiB
Swift
Executable File
import SwiftUI
|
||
|
||
struct ClockView: View {
|
||
@State private var currentTime = Date()
|
||
@Environment(\.colorScheme) var colorScheme
|
||
let timer = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect()
|
||
|
||
var body: some View {
|
||
VStack {
|
||
Text("360 Clock")
|
||
.font(.largeTitle)
|
||
.fontWeight(.bold)
|
||
.padding(.top)
|
||
|
||
Text(timeString)
|
||
.font(.title2)
|
||
.foregroundColor(.secondary)
|
||
.padding(.bottom, 10)
|
||
|
||
GeometryReader { geometry in
|
||
let size = min(geometry.size.width, geometry.size.height)
|
||
|
||
ZStack {
|
||
// Циферблат
|
||
Circle()
|
||
.stroke(colorScheme == .dark ? Color.white : Color.black, lineWidth: 2)
|
||
.frame(width: size * 0.8, height: size * 0.8)
|
||
|
||
// Метки на циферблате (каждые 15 градусов)
|
||
ForEach(0..<24, id: \.self) { index in
|
||
let angle = Double(index * 15) // 0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255, 270, 285, 300, 315, 330, 345
|
||
let radius = size * 0.35
|
||
// Начинаем с 0 вверху (угол -90 градусов от стандартного)
|
||
let adjustedAngle = angle - 90
|
||
let x = cos(adjustedAngle * .pi / 180) * radius
|
||
let y = sin(adjustedAngle * .pi / 180) * radius
|
||
|
||
// Показываем градусы
|
||
Text("\(index * 15)")
|
||
.font(.caption)
|
||
.fontWeight(.bold)
|
||
.foregroundColor(colorScheme == .dark ? Color.white : Color.black)
|
||
.offset(x: x, y: y)
|
||
}
|
||
|
||
// Центральная точка
|
||
Circle()
|
||
.fill(Color.red)
|
||
.frame(width: 8, height: 8)
|
||
|
||
// Секундная стрелка (60 оборотов за 1 оборот минутной стрелки)
|
||
ClockHand(angle: secondAngle, length: size * 0.35, width: 1, color: colorScheme == .dark ? Color.white : Color.black)
|
||
|
||
// Минутная стрелка (1 оборот за градус часовой стрелки)
|
||
ClockHand(angle: minuteAngle, length: size * 0.25, width: 2, color: .blue)
|
||
|
||
// Часовая стрелка (24-часовой формат)
|
||
ClockHand(angle: hourAngle, length: size * 0.2, width: 4, color: .red)
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
}
|
||
}
|
||
.onReceive(timer) { input in
|
||
currentTime = input
|
||
}
|
||
}
|
||
|
||
private var timeString: String {
|
||
let formatter = DateFormatter()
|
||
formatter.timeStyle = .medium
|
||
return formatter.string(from: currentTime)
|
||
}
|
||
|
||
// Угол часовой стрелки для 24-часового формата
|
||
private var hourAngle: Double {
|
||
let calendar = Calendar.current
|
||
let hour = calendar.component(.hour, from: currentTime)
|
||
let minute = calendar.component(.minute, from: currentTime)
|
||
let second = calendar.component(.second, from: currentTime)
|
||
|
||
// 24-часовой формат: 15° = 1 час
|
||
// 0 часов = 0°, 1 час = 15°, 2 часа = 30°, ..., 24 часов = 360°
|
||
let baseHourAngle = Double(hour) * 15.0
|
||
|
||
// Добавляем плавное движение для минут и секунд
|
||
let minuteOffset = Double(minute) * 0.25 // 15° / 60 минут = 0.25° за минуту
|
||
let secondOffset = Double(second) * 0.0042 // 15° / 3600 секунд = 0.0042° за секунду
|
||
let totalAngle = baseHourAngle + minuteOffset + secondOffset
|
||
|
||
// В SwiftUI: 0° = вверх, 90° = вправо, 180° = вниз, 270° = влево
|
||
// Нам нужно: 0 часов = вверх (0°), 6 часов = вправо (90°), 12 часов = вниз (180°), 18 часов = влево (270°)
|
||
// Простая формула: totalAngle
|
||
return totalAngle
|
||
}
|
||
|
||
// Угол минутной стрелки (1 оборот за градус часовой стрелки)
|
||
private var minuteAngle: Double {
|
||
// Минутная стрелка делает 1 оборот (360°) за каждый градус поворота часовой стрелки
|
||
// hourAngle уже в градусах, поэтому просто умножаем на 360
|
||
let totalAngle = hourAngle * 360.0
|
||
// Приводим к диапазону 0-360°
|
||
return totalAngle.truncatingRemainder(dividingBy: 360.0)
|
||
}
|
||
|
||
// Угол секундной стрелки (60 оборотов за 1 оборот минутной стрелки)
|
||
private var secondAngle: Double {
|
||
// Секундная стрелка делает 60 оборотов за 1 оборот минутной стрелки
|
||
let totalAngle = minuteAngle * 60.0
|
||
// Приводим к диапазону 0-360°
|
||
return totalAngle.truncatingRemainder(dividingBy: 360.0)
|
||
}
|
||
|
||
}
|
||
|
||
struct ClockHand: View {
|
||
let angle: Double
|
||
let length: CGFloat
|
||
let width: CGFloat
|
||
let color: Color
|
||
|
||
var body: some View {
|
||
Rectangle()
|
||
.fill(color)
|
||
.frame(width: width, height: length)
|
||
.offset(y: -length/2) // Смещаем вверх, чтобы центр был в центре циферблата
|
||
.rotationEffect(.degrees(angle)) // Поворачиваем на нужный угол
|
||
}
|
||
}
|
||
|
||
struct ClockView_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
ClockView()
|
||
}
|
||
}
|