Добавить полноценный Widget Extension и исправить отображение циферблата в виджете.

Вынесена реализация виджета в отдельный target с корректным embedding и Info.plist, чтобы сборка и запуск работали стабильно на устройстве, а также улучшена адаптивная верстка меток и фонового контейнера для соответствия требованиям WidgetKit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-17 18:59:07 +03:00
parent a3bf27169b
commit 0697440437
8 changed files with 501 additions and 26 deletions

View File

@@ -0,0 +1,195 @@
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))
}
}