Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Bedtime/Bedtime/BedtimeApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,32 @@ struct BedtimeApp: App {
}
}()

init() {
Self.seedDefaultPreferencesIfNeeded(in: sharedModelContainer)
}

/// Creates the single `UserPreferences` record when the store is empty.
///
/// SwiftData persists the schema but never creates instances, so a fresh install —
/// or a launch right after the store reset above — starts with nothing to read.
/// Seeding here, before the view tree exists, keeps the insert out of `ContentView`'s
/// body evaluation: SwiftUI reads a view's properties an unspecified number of times
/// per update, so inserting from there can write several default records and mutates
/// persistent state in the middle of a render.
@MainActor
private static func seedDefaultPreferencesIfNeeded(in container: ModelContainer) {
let context = container.mainContext

var descriptor = FetchDescriptor<UserPreferences>()
descriptor.fetchLimit = 1

// A failed fetch is not a reason to insert; ContentView still seeds defensively.
guard let existing = try? context.fetch(descriptor), existing.isEmpty else { return }

context.insert(UserPreferences())
try? context.save()
}

var body: some Scene {
WindowGroup {
ContentView()
Expand Down
69 changes: 43 additions & 26 deletions Bedtime/Bedtime/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,7 @@ struct ContentView: View {
return ViewModel.recentSourceAppLinks(sleepSessions: healthKitManager.allSleepSessions)
}

private var userPreferences: UserPreferences {
if let existing = preferences.first {
return existing
} else {
let new = UserPreferences()
modelContext.insert(new)
return new
}
}

private var sleepBank: SleepBank {
private func sleepBank(for userPreferences: UserPreferences) -> SleepBank {
ViewModel.calculateSleepBank(
sleepSessions: healthKitManager.sleepSessions,
goalHours: userPreferences.sleepGoalHours,
Expand All @@ -61,22 +51,25 @@ struct ContentView: View {

/// Balance over the widest selectable lookback, independent of the current range, so the
/// balance chart can show every night the range start can be moved to.
private var fullWindowSleepBank: SleepBank {
private func fullWindowSleepBank(for userPreferences: UserPreferences) -> SleepBank {
ViewModel.calculateSleepBank(
sleepSessions: healthKitManager.sleepSessions,
goalHours: userPreferences.sleepGoalHours,
recentDays: Constants.sleepBankDaysRange.upperBound
)
}

private var sleepBankDaysBinding: Binding<Int> {
private func sleepBankDaysBinding(for userPreferences: UserPreferences) -> Binding<Int> {
Binding(
get: { userPreferences.sleepBankDays },
set: { userPreferences.sleepBankDays = $0 }
)
}

private var bedtimeRecommendation: BedtimeRecommendation {
private func bedtimeRecommendation(
for userPreferences: UserPreferences,
sleepBank: SleepBank
) -> BedtimeRecommendation {
ViewModel.generateBedtimeRecommendation(
wakeTime: userPreferences.wakeTime,
earliestBedtime: userPreferences.earliestReasonableBedtime,
Expand All @@ -86,14 +79,14 @@ struct ContentView: View {
)
}

private var wakeTimeBinding: Binding<Date> {
private func wakeTimeBinding(for userPreferences: UserPreferences) -> Binding<Date> {
Binding(
get: { userPreferences.wakeTime },
set: { userPreferences.wakeTime = $0 }
)
}

private var sleepBankInsight: SleepBankInsight? {
private func sleepBankInsight(for userPreferences: UserPreferences) -> SleepBankInsight? {
SleepInsightsEngine.generateInsight(
sleepSessions: healthKitManager.sleepSessions,
goalHours: userPreferences.sleepGoalHours,
Expand All @@ -104,8 +97,32 @@ struct ContentView: View {
)
}

/// `BedtimeApp` seeds the record before the view tree is built, so the placeholder
/// below is only reachable if that seed failed — or in previews and tests, which
/// start from an empty in-memory container.
var body: some View {
if let userPreferences = preferences.first {
dashboard(for: userPreferences)
} else {
ProgressView()
.task { seedDefaultPreferencesIfNeeded() }
}
}

private func seedDefaultPreferencesIfNeeded() {
guard preferences.isEmpty else { return }
modelContext.insert(UserPreferences())
}

@ViewBuilder
private func dashboard(for userPreferences: UserPreferences) -> some View {
let isBeforeEvening = Calendar.current.component(.hour, from: Date()) < 18
let currentSleepBank = sleepBank(for: userPreferences)
let recommendation = bedtimeRecommendation(for: userPreferences, sleepBank: currentSleepBank)
let insight = sleepBankInsight(for: userPreferences)
let daysBinding = sleepBankDaysBinding(for: userPreferences)
let wakeBinding = wakeTimeBinding(for: userPreferences)

NavigationStack {
ScrollView {
VStack(spacing: 20) {
Expand All @@ -124,20 +141,20 @@ struct ContentView: View {
sourceAppLinks: recentSourceAppLinks)
} else {
BedtimeRecommendationCard(
recommendation: bedtimeRecommendation,
wakeTime: wakeTimeBinding
recommendation: recommendation,
wakeTime: wakeBinding
)
}

SleepBankCard(
sleepBank: sleepBank,
fullWindowBank: fullWindowSleepBank,
sleepBankDays: sleepBankDaysBinding
sleepBank: currentSleepBank,
fullWindowBank: fullWindowSleepBank(for: userPreferences),
sleepBankDays: daysBinding
)

if let sleepBankInsight {
if let insight {
SleepInsightsCard(
insight: sleepBankInsight,
insight: insight,
currentSleepBankDays: userPreferences.sleepBankDays,
onApplyDays: { days in
withAnimation(.easeInOut(duration: 0.2)) {
Expand All @@ -149,8 +166,8 @@ struct ContentView: View {

if isBeforeEvening {
BedtimeRecommendationCard(
recommendation: bedtimeRecommendation,
wakeTime: wakeTimeBinding
recommendation: recommendation,
wakeTime: wakeBinding
)
} else {
LastNightCard(sleepSessions: lastNightData,
Expand All @@ -165,7 +182,7 @@ struct ContentView: View {
allSessions: healthKitManager.allSleepSessions,
excludedSourceIDs: sourcePreferences.excludedBundleIdentifiers,
sleepGoal: userPreferences.sleepGoalHours,
sleepBankDays: sleepBankDaysBinding
sleepBankDays: daysBinding
)
}
}
Expand Down