import SwiftUI struct SleepDetailView: View { let sleep: SleepData @StateObject private var healthKit = HealthKitService() @State private var segments: [SleepSegment] = [] @State private var isLoading = true @Environment(\.dismiss) var dismiss var total: Double { sleep.totalSleep ?? 0 } var deep: Double { sleep.deep ?? 0 } var rem: Double { sleep.rem ?? 0 } var core: Double { sleep.core ?? 0 } var phases: [(name: String, value: Double, color: Color, icon: String)] { [ ("Глубокий", deep, SleepPhaseType.deep.color, "moon.zzz.fill"), ("REM", rem, SleepPhaseType.rem.color, "brain.head.profile"), ("Базовый", core, SleepPhaseType.core.color, "moon.fill"), ] } var sleepStart: Date? { segments.first?.start } var sleepEnd: Date? { segments.last?.end } var body: some View { ZStack { Color(hex: "06060f").ignoresSafeArea() ScrollView(showsIndicators: false) { VStack(spacing: 20) { // Header HStack { Button(action: { dismiss() }) { Image(systemName: "xmark.circle.fill") .font(.title2).foregroundColor(Theme.textSecondary) } Spacer() Text("Анализ сна").font(.headline).foregroundColor(.white) Spacer() Color.clear.frame(width: 28) } .padding(.horizontal).padding(.top, 16) // Total VStack(spacing: 8) { Text(String(format: "%.1f ч", total)) .font(.system(size: 48, weight: .bold, design: .rounded)) .foregroundColor(Theme.purple) if let start = sleepStart, let end = sleepEnd { Text("\(fmt(start)) — \(fmt(end))") .font(.callout).foregroundColor(Theme.textSecondary) } } .padding(.vertical, 4) // Phase cards HStack(spacing: 12) { ForEach(phases, id: \.name) { phase in VStack(spacing: 6) { Image(systemName: phase.icon).font(.caption).foregroundColor(phase.color) Text(fmtDuration(phase.value)).font(.callout.bold().monospacedDigit()).foregroundColor(.white) Text(phase.name).font(.caption2).foregroundColor(Theme.textSecondary) if total > 0 { Text("\(Int(phase.value / total * 100))%").font(.caption2.bold()).foregroundColor(phase.color) } } .frame(maxWidth: .infinity).padding(.vertical, 12) .glassCard(cornerRadius: 14) } } .padding(.horizontal) // Hypnogram if isLoading { ProgressView().tint(Theme.purple).padding(.top, 20) } else if !segments.isEmpty { VStack(alignment: .leading, spacing: 8) { Text("Гипнограмма").font(.subheadline.bold()).foregroundColor(.white) HypnogramView(segments: segments) .frame(height: 180) } .padding(16) .glassCard(cornerRadius: 16) .padding(.horizontal) } else { VStack(spacing: 8) { Image(systemName: "moon.zzz").font(.title).foregroundColor(Theme.textSecondary) Text("График недоступен").font(.subheadline).foregroundColor(Theme.textSecondary) }.padding(.top, 20) } // Stacked bar VStack(alignment: .leading, spacing: 8) { Text("Распределение").font(.subheadline.bold()).foregroundColor(.white) GeometryReader { geo in HStack(spacing: 2) { ForEach(phases, id: \.name) { phase in let frac = total > 0 ? phase.value / total : 0 RoundedRectangle(cornerRadius: 4) .fill(phase.color) .frame(width: max(geo.size.width * CGFloat(frac), frac > 0 ? 4 : 0)) .shadow(color: phase.color.opacity(0.4), radius: 4) } } } .frame(height: 14) } .padding(16) .glassCard(cornerRadius: 16) .padding(.horizontal) // Legend HStack(spacing: 16) { ForEach([SleepPhaseType.awake, .rem, .core, .deep], id: \.rawValue) { phase in HStack(spacing: 4) { Circle().fill(phase.color).frame(width: 8, height: 8) Text(phase.rawValue).font(.caption2).foregroundColor(Theme.textSecondary) } } } Spacer(minLength: 40) } } } .task { if healthKit.isAvailable { try? await healthKit.requestAuthorization() segments = await healthKit.fetchSleepSegments() } isLoading = false } } private func fmt(_ date: Date) -> String { let f = DateFormatter(); f.dateFormat = "HH:mm"; return f.string(from: date) } private func fmtDuration(_ h: Double) -> String { let hrs = Int(h); let mins = Int((h - Double(hrs)) * 60) if hrs > 0 { return "\(hrs)ч \(mins)м" } return "\(mins)м" } } // MARK: - Hypnogram (sleep stages chart) struct HypnogramView: View { let segments: [SleepSegment] // Phase levels: awake=top, rem, core, deep=bottom private func yLevel(_ phase: SleepPhaseType) -> CGFloat { switch phase { case .awake: return 0.0 case .rem: return 0.33 case .core: return 0.66 case .deep: return 1.0 } } private var timeStart: Date { segments.first?.start ?? Date() } private var timeEnd: Date { segments.last?.end ?? Date() } private var totalSpan: TimeInterval { max(timeEnd.timeIntervalSince(timeStart), 1) } var body: some View { GeometryReader { geo in let w = geo.size.width let chartH = geo.size.height - 36 // space for labels ZStack(alignment: .topLeading) { // Grid lines + labels ForEach(0..<4, id: \.self) { i in let y = chartH * CGFloat(i) / 3.0 Path { p in p.move(to: CGPoint(x: 0, y: y)); p.addLine(to: CGPoint(x: w, y: y)) } .stroke(Color.white.opacity(0.05), lineWidth: 1) let labels = ["Пробуждение", "REM", "Базовый", "Глубокий"] Text(labels[i]) .font(.system(size: 8)) .foregroundColor(Color.white.opacity(0.25)) .position(x: 35, y: y) } // Filled step areas ForEach(segments) { seg in let x1 = w * CGFloat(seg.start.timeIntervalSince(timeStart) / totalSpan) let x2 = w * CGFloat(seg.end.timeIntervalSince(timeStart) / totalSpan) let segW = max(x2 - x1, 1) let y = yLevel(seg.phase) * chartH // Fill from phase level to bottom Rectangle() .fill(seg.phase.color.opacity(0.15)) .frame(width: segW, height: chartH - y) .position(x: x1 + segW / 2, y: y + (chartH - y) / 2) // Top edge highlight Rectangle() .fill(seg.phase.color) .frame(width: segW, height: 3) .shadow(color: seg.phase.color.opacity(0.6), radius: 4, y: 0) .position(x: x1 + segW / 2, y: y) } // Step line connecting phases Path { path in for (i, seg) in segments.enumerated() { let x = w * CGFloat(seg.start.timeIntervalSince(timeStart) / totalSpan) let y = yLevel(seg.phase) * chartH let xEnd = w * CGFloat(seg.end.timeIntervalSince(timeStart) / totalSpan) if i == 0 { path.move(to: CGPoint(x: x, y: y)) } else { path.addLine(to: CGPoint(x: x, y: y)) } path.addLine(to: CGPoint(x: xEnd, y: y)) } } .stroke(Color.white.opacity(0.5), style: StrokeStyle(lineWidth: 1.5)) // Time labels at bottom let hours = timeLabels() ForEach(hours, id: \.1) { (date, label) in let x = w * CGFloat(date.timeIntervalSince(timeStart) / totalSpan) Text(label) .font(.system(size: 9)) .foregroundColor(Theme.textSecondary) .position(x: x, y: chartH + 18) } } } } private func timeLabels() -> [(Date, String)] { let cal = Calendar.current let f = DateFormatter(); f.dateFormat = "HH:mm" var labels: [(Date, String)] = [] var date = cal.date(bySetting: .minute, value: 0, of: timeStart) ?? timeStart if date < timeStart { date = cal.date(byAdding: .hour, value: 1, to: date) ?? date } while date < timeEnd { labels.append((date, f.string(from: date))) date = cal.date(byAdding: .hour, value: 1, to: date) ?? timeEnd } // Keep max 6 labels to avoid overlap if labels.count > 6 { let step = labels.count / 5 labels = stride(from: 0, to: labels.count, by: step).map { labels[$0] } } return labels } }