Files
360Clock/ClockWidgetExtension/ClockWidgetBundle.swift
bilal 0697440437 Добавить полноценный Widget Extension и исправить отображение циферблата в виджете.
Вынесена реализация виджета в отдельный target с корректным embedding и Info.plist, чтобы сборка и запуск работали стабильно на устройстве, а также улучшена адаптивная верстка меток и фонового контейнера для соответствия требованиям WidgetKit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 18:59:07 +03:00

196 lines
6.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ClockEntry>) -> 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))
}
}