Добавить полноценный Widget Extension и исправить отображение циферблата в виджете.
Вынесена реализация виджета в отдельный target с корректным embedding и Info.plist, чтобы сборка и запуск работали стабильно на устройстве, а также улучшена адаптивная верстка меток и фонового контейнера для соответствия требованиям WidgetKit. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
195
ClockWidgetExtension/ClockWidgetBundle.swift
Normal file
195
ClockWidgetExtension/ClockWidgetBundle.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
29
ClockWidgetExtension/Info.plist
Normal file
29
ClockWidgetExtension/Info.plist
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>360 Clock Widget</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user