import SwiftUI import WidgetKit struct ClockEntry: TimelineEntry { let date: Date let hourAngle: Double let minuteAngle: Double let secondAngle: Double } struct ClockProvider: TimelineProvider { func placeholder(in context: Context) -> ClockEntry { ClockEntry(date: Date(), hourAngle: 0, minuteAngle: 0, secondAngle: 0) } func getSnapshot(in context: Context, completion: @escaping (ClockEntry) -> Void) { completion(makeEntry(for: Date())) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let now = Date() let entry = makeEntry(for: now) // Widgets are budget-limited; minute updates are realistic for production behavior. let nextUpdate = Calendar.current.date(byAdding: .minute, value: 1, to: now) ?? now.addingTimeInterval(60) completion(Timeline(entries: [entry], policy: .after(nextUpdate))) } private func makeEntry(for date: Date) -> ClockEntry { let hourAngle = calculateHourAngle(date: date) let minuteAngle = (hourAngle * 360.0).truncatingRemainder(dividingBy: 360.0) let secondAngle = (minuteAngle * 60.0).truncatingRemainder(dividingBy: 360.0) return ClockEntry(date: date, hourAngle: hourAngle, minuteAngle: minuteAngle, secondAngle: secondAngle) } private func calculateHourAngle(date: Date) -> Double { let calendar = Calendar.current let hour = calendar.component(.hour, from: date) let minute = calendar.component(.minute, from: date) let second = calendar.component(.second, from: date) let baseHourAngle = Double(hour) * 15.0 let minuteOffset = Double(minute) * 0.25 let secondOffset = Double(second) * 0.0042 return baseHourAngle + minuteOffset + secondOffset } } struct ClockWidgetView: View { @Environment(\.widgetFamily) private var family let entry: ClockEntry private var labelStep: Int { switch family { case .systemSmall: return 3 // 0,45,90... для компактного размера case .systemMedium: return 2 // 0,30,60... для лучшей читаемости default: return 1 } } private var labelFont: Font { switch family { case .systemSmall: return .system(size: 7, weight: .bold, design: .rounded) case .systemMedium: return .system(size: 8, weight: .bold, design: .rounded) default: return .caption2 } } private var titleFont: Font { family == .systemSmall ? .system(size: 10, weight: .bold) : .caption } private var titleBottomPadding: CGFloat { family == .systemSmall ? 2 : 6 } private var dialScale: CGFloat { family == .systemSmall ? 0.88 : 0.84 } private var labelRadiusRatio: CGFloat { family == .systemSmall ? 0.31 : 0.34 } private var handConfig: (second: (CGFloat, CGFloat), minute: (CGFloat, CGFloat), hour: (CGFloat, CGFloat), center: CGFloat) { if family == .systemSmall { return ((0.30, 1.0), (0.22, 1.4), (0.17, 2.4), 3.5) } return ((0.35, 1.0), (0.25, 1.5), (0.2, 3.0), 4.0) } var body: some View { VStack(spacing: 0) { Text("360 Clock") .font(titleFont) .padding(.bottom, titleBottomPadding) GeometryReader { geometry in let size = min(geometry.size.width, geometry.size.height) let dialSize = size * dialScale ZStack { Circle() .stroke(Color.primary, lineWidth: 1) .frame(width: dialSize, height: dialSize) ForEach(0..<24, id: \.self) { index in if index % labelStep == 0 { let angle = Double(index * 15) - 90 let radius = dialSize * labelRadiusRatio let x = cos(angle * .pi / 180) * radius let y = sin(angle * .pi / 180) * radius Text("\(index * 15)") .font(labelFont) .monospacedDigit() .lineLimit(1) .minimumScaleFactor(0.6) .offset(x: x, y: y) } } Circle() .fill(.red) .frame(width: handConfig.center, height: handConfig.center) ClockHand(angle: entry.secondAngle, length: dialSize * handConfig.second.0, width: handConfig.second.1, color: .primary) ClockHand(angle: entry.minuteAngle, length: dialSize * handConfig.minute.0, width: handConfig.minute.1, color: .blue) ClockHand(angle: entry.hourAngle, length: dialSize * handConfig.hour.0, width: handConfig.hour.1, color: .red) } .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center) } } .padding(8) .widgetBackgroundCompat() } } private extension View { @ViewBuilder func widgetBackgroundCompat() -> some View { if #available(iOS 17.0, *) { self.containerBackground(.fill.tertiary, for: .widget) } else { self } } } 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 ClockWidget: Widget { let kind = "ClockWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: ClockProvider()) { entry in ClockWidgetView(entry: entry) } .configurationDisplayName("360 Clock") .description("Показывает время с 24-часовым 360° циферблатом.") .supportedFamilies([.systemSmall, .systemMedium]) } } @main struct ClockWidgetBundle: WidgetBundle { var body: some Widget { ClockWidget() } } struct ClockWidget_Previews: PreviewProvider { static var previews: some View { ClockWidgetView(entry: ClockEntry(date: Date(), hourAngle: 90, minuteAngle: 180, secondAngle: 270)) .previewContext(WidgetPreviewContext(family: .systemSmall)) } }