import WidgetKit import SwiftUI // MARK: - Data struct HealthEntry: TimelineEntry { let date: Date let steps: Int let sleep: Double let heartRate: Int let readinessScore: Int static let placeholder = HealthEntry(date: Date(), steps: 6234, sleep: 7.5, heartRate: 68, readinessScore: 80) } // MARK: - Provider struct HealthProvider: TimelineProvider { func placeholder(in context: Context) -> HealthEntry { .placeholder } func getSnapshot(in context: Context, completion: @escaping (HealthEntry) -> Void) { completion(.placeholder) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { Task { let entry = await fetchHealthData() let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date()) ?? Date() completion(Timeline(entries: [entry], policy: .after(nextUpdate))) } } private func fetchHealthData() async -> HealthEntry { let baseURL = "https://health.digital-home.site" guard let token = KeychainService.load(key: KeychainService.healthTokenKey) else { return .placeholder } // Fetch latest var steps = 0, sleep = 0.0, hr = 0 if let url = URL(string: "\(baseURL)/api/health/latest") { var req = URLRequest(url: url) req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") req.timeoutInterval = 10 if let (data, resp) = try? await URLSession.shared.data(for: req), (resp as? HTTPURLResponse)?.statusCode == 200, let json = try? JSONDecoder().decode(WidgetHealthLatest.self, from: data) { steps = json.steps?.total ?? 0 sleep = json.sleep?.totalSleep ?? 0 hr = Int(json.restingHeartRate?.value ?? 0) } } // Fetch readiness var score = 0 if let url = URL(string: "\(baseURL)/api/health/readiness") { var req = URLRequest(url: url) req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") req.timeoutInterval = 10 if let (data, resp) = try? await URLSession.shared.data(for: req), (resp as? HTTPURLResponse)?.statusCode == 200, let json = try? JSONDecoder().decode(WidgetReadiness.self, from: data) { score = json.score } } return HealthEntry(date: Date(), steps: steps, sleep: sleep, heartRate: hr, readinessScore: score) } } // Lightweight models struct WidgetHealthLatest: Codable { let steps: WidgetSteps? let sleep: WidgetSleep? let restingHeartRate: WidgetRHR? } struct WidgetSteps: Codable { let total: Int? } struct WidgetSleep: Codable { let totalSleep: Double? } struct WidgetRHR: Codable { let value: Double? } struct WidgetReadiness: Codable { let score: Int } // MARK: - Widget struct HealthSummaryWidget: Widget { let kind = "HealthSummary" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: HealthProvider()) { entry in HealthWidgetView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Здоровье") .description("Шаги, сон, пульс и готовность") .supportedFamilies([.systemSmall, .systemMedium]) } } // MARK: - Views struct HealthWidgetView: View { let entry: HealthEntry @Environment(\.widgetFamily) var family var body: some View { switch family { case .systemSmall: smallView case .systemMedium: mediumView default: smallView } } var readinessColor: Color { if entry.readinessScore >= 80 { return Color(hex: "0D9488") } if entry.readinessScore >= 60 { return Color(hex: "ffa502") } return Color(hex: "ff4757") } var smallView: some View { VStack(spacing: 8) { // Readiness ZStack { Circle().stroke(Color.white.opacity(0.1), lineWidth: 6).frame(width: 50, height: 50) Circle().trim(from: 0, to: CGFloat(entry.readinessScore) / 100) .stroke(readinessColor, style: StrokeStyle(lineWidth: 6, lineCap: .round)) .frame(width: 50, height: 50).rotationEffect(.degrees(-90)) Text("\(entry.readinessScore)").font(.system(size: 16, weight: .bold, design: .rounded)).foregroundColor(readinessColor) } VStack(spacing: 4) { HStack(spacing: 4) { Image(systemName: "figure.walk").font(.system(size: 9)).foregroundColor(Color(hex: "ffa502")) Text("\(entry.steps)").font(.caption2.bold()).foregroundColor(.white) } HStack(spacing: 4) { Image(systemName: "moon.fill").font(.system(size: 9)).foregroundColor(Color(hex: "7c3aed")) Text(String(format: "%.1fч", entry.sleep)).font(.caption2.bold()).foregroundColor(.white) } } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(hex: "06060f")) } var mediumView: some View { HStack(spacing: 16) { // Readiness ring ZStack { Circle().stroke(Color.white.opacity(0.1), lineWidth: 8).frame(width: 68, height: 68) Circle().trim(from: 0, to: CGFloat(entry.readinessScore) / 100) .stroke(readinessColor, style: StrokeStyle(lineWidth: 8, lineCap: .round)) .frame(width: 68, height: 68).rotationEffect(.degrees(-90)) VStack(spacing: 0) { Text("\(entry.readinessScore)").font(.system(size: 20, weight: .bold, design: .rounded)).foregroundColor(readinessColor) Text("балл").font(.system(size: 9)).foregroundColor(.gray) } } // Metrics VStack(alignment: .leading, spacing: 6) { Text("Здоровье").font(.subheadline.bold()).foregroundColor(.white) HStack(spacing: 14) { HealthBadge(icon: "figure.walk", value: "\(entry.steps)", color: Color(hex: "ffa502")) HealthBadge(icon: "moon.fill", value: String(format: "%.1fч", entry.sleep), color: Color(hex: "7c3aed")) HealthBadge(icon: "heart.fill", value: "\(entry.heartRate)", color: Color(hex: "ff4757")) } } } .padding(16) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(hex: "06060f")) } } struct HealthBadge: View { let icon: String let value: String let color: Color var body: some View { VStack(spacing: 3) { Image(systemName: icon).font(.caption).foregroundColor(color) Text(value).font(.caption2.bold()).foregroundColor(.white) } } }