diff --git a/.vscode/settings.json b/.vscode/settings.json index 3503ee7..7808894 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,6 @@ }, "sweetpad.xcodebuildserver.serverEnv": { "DEVELOPER_DIR": "/Applications/Xcode.app/Contents/Developer" - } + }, + "diffEditor.ignoreTrimWhitespace": true } diff --git a/Bedtime/Bedtime/Constants.swift b/Bedtime/Bedtime/Constants.swift index ba9082d..945e4e9 100644 --- a/Bedtime/Bedtime/Constants.swift +++ b/Bedtime/Bedtime/Constants.swift @@ -9,4 +9,5 @@ import Foundation class Constants { static let iconWidth: CGFloat = 30 + static let sleepHistoryDays = 30 } diff --git a/Bedtime/Bedtime/Models/HealthKitManager.swift b/Bedtime/Bedtime/Models/HealthKitManager.swift index ebad451..9889942 100644 --- a/Bedtime/Bedtime/Models/HealthKitManager.swift +++ b/Bedtime/Bedtime/Models/HealthKitManager.swift @@ -83,7 +83,11 @@ class HealthKitManager: ObservableObject { private func fetchSleepDataForDisplay() async throws { let calendar = Calendar.current let endDate = Date() - guard let startDate = calendar.date(byAdding: .day, value: -30, to: endDate) else { + let today = calendar.startOfDay(for: endDate) + // Fetch one extra day before the UI range: grouping (midpoint + 6h) can assign + // sessions that start the previous evening to the oldest displayed day, including + // short blocks (e.g. 9–11pm) as well as overnight sleep. + guard let startDate = calendar.date(byAdding: .day, value: -Constants.sleepHistoryDays, to: today) else { throw NSError(domain: "HealthKitManager", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to calculate start date"]) } diff --git a/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift b/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift index ce2a9cf..d306609 100644 --- a/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift +++ b/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift @@ -33,6 +33,10 @@ struct SleepDayGroup: View { return (abs(difference), difference >= 0, color) } + private var hasSessions: Bool { + !sessions.isEmpty + } + var body: some View { VStack(spacing: 0) { // Day header (always visible) @@ -43,41 +47,52 @@ struct SleepDayGroup: View { .font(.subheadline) .fontWeight(.medium) - Text("\(timeFormatter.string(from: sessions.last?.startDate ?? Date())) - \(timeFormatter.string(from: sessions.first?.endDate ?? Date()))") - .font(.caption) - .foregroundColor(.secondary) + if hasSessions { + Text("\(timeFormatter.string(from: sessions.last!.startDate)) - \(timeFormatter.string(from: sessions.first!.endDate))") + .font(.caption) + .foregroundColor(.secondary) + } else { + Text("No sleep data") + .font(.caption) + .foregroundColor(.secondary) + } } Spacer() VStack(alignment: .trailing, spacing: 2) { - Text(TimeFormatter.formatDuration(sessions.reduce(0) { $0 + $1.duration })) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.primary) - - HStack(spacing: 2) { - Text(balanceImpact.isPositive ? "+" : "-") - .font(.caption) - .foregroundColor(balanceImpact.color) + if hasSessions { + Text(TimeFormatter.formatDuration(sessions.reduce(0) { $0 + $1.duration })) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.primary) - Text(String(format: "%.1fh", balanceImpact.value)) - .font(.caption) - .foregroundColor(balanceImpact.color) + HStack(spacing: 2) { + Text(balanceImpact.isPositive ? "+" : "-") + .font(.caption) + .foregroundColor(balanceImpact.color) + + Text(String(format: "%.1fh", balanceImpact.value)) + .font(.caption) + .foregroundColor(balanceImpact.color) + } } } - Image(systemName: isExpanded ? "chevron.up" : "chevron.down") - .font(.caption) - .foregroundColor(.secondary) + if hasSessions { + Image(systemName: isExpanded ? "chevron.up" : "chevron.down") + .font(.caption) + .foregroundColor(.secondary) + } } .padding(.vertical, 8) .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) + .disabled(!hasSessions) // Session details (expandable) - if isExpanded { + if isExpanded && hasSessions { VStack(spacing: 4) { ForEach(Array(sessions.enumerated()), id: \.offset) { index, session in SleepSessionRow(session: session) diff --git a/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift b/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift index e061641..aad860f 100644 --- a/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift +++ b/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift @@ -9,10 +9,17 @@ import SwiftUI struct RecentSleepSessionsCard: View { - init(sessions: [Date: [SleepSession]], sleepGoal: Double) { + init(sessions: [Date: [SleepSession]], sleepGoal: Double, dayCount: Int = Constants.sleepHistoryDays) { self.sessions = sessions - self.sortedSessions = sessions.sorted { $0.key > $1.key } self.sleepGoal = sleepGoal + + let calendar = Calendar.current + let today = calendar.startOfDay(for: Date()) + self.sortedSessions = (0.. (Date, [SleepSession])? in + guard let day = calendar.date(byAdding: .day, value: -offset, to: today) else { return nil } + let dayStart = calendar.startOfDay(for: day) + return (dayStart, sessions[dayStart] ?? []) + } } let sessions: [Date: [SleepSession]]