TheNoiseClock/TheNoiseClock/App/ContentView.swift

157 lines
5.4 KiB
Swift

//
// ContentView.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
import Bedrock
/// Main tab navigation coordinator
struct ContentView: View {
// MARK: - Properties
private enum Tab: Hashable, CustomStringConvertible {
case clock
case alarms
case noise
case settings
var description: String {
switch self {
case .clock: return "clock"
case .alarms: return "alarms"
case .noise: return "noise"
case .settings: return "settings"
}
}
}
@State private var selectedTab: Tab = .clock
@State private var clockViewModel = ClockViewModel()
@State private var alarmViewModel = AlarmViewModel()
@State private var onboardingState = OnboardingState(appIdentifier: "TheNoiseClock")
@State private var keepAwakePromptState = KeepAwakePromptState()
// MARK: - Computed Properties
/// Whether the clock tab is currently selected - passed to ClockView to prevent race conditions
private var isOnClockTab: Bool {
selectedTab == .clock
}
// MARK: - Body
var body: some View {
ZStack {
// Main tab content
TabView(selection: $selectedTab) {
NavigationStack {
// Pass isOnClockTab so ClockView can make the right tab bar decision
// Tab bar hides ONLY when: isOnClockTab && isDisplayMode
// This prevents race conditions on tab switch
ClockView(viewModel: clockViewModel, isOnClockTab: isOnClockTab)
}
.tabItem {
Label("Clock", systemImage: "clock")
}
.tag(Tab.clock)
NavigationStack {
AlarmView(viewModel: alarmViewModel)
}
.tabItem {
Label("Alarms", systemImage: "alarm")
}
.tag(Tab.alarms)
NavigationStack {
NoiseView()
}
.tabItem {
Label("Noise", systemImage: "waveform")
}
.tag(Tab.noise)
NavigationStack {
ClockSettingsView(
style: clockViewModel.style,
onCommit: { newStyle in
clockViewModel.updateStyle(newStyle)
},
onResetOnboarding: {
onboardingState.reset()
}
)
}
.tabItem {
Label("Settings", systemImage: "gearshape")
}
.tag(Tab.settings)
}
.onChange(of: selectedTab) { oldValue, newValue in
Design.debugLog("[ContentView] Tab changed: \(oldValue) -> \(newValue)")
if oldValue == .clock && newValue != .clock {
Design.debugLog("[ContentView] Leaving clock tab, setting fullScreenMode to false")
// Safety net: also explicitly disable full-screen mode when leaving clock tab
// The ClockView's toolbar modifier already responds to isOnClockTab changing
clockViewModel.setFullScreenMode(false)
}
}
.accentColor(AppAccent.primary)
.background(Color.Branding.primary.ignoresSafeArea())
// Note: AlarmKit handles the alarm UI via the system Lock Screen and Dynamic Island.
// No in-app alarm screen is needed - users interact with alarms via the system UI.
// Onboarding overlay for first-time users
if !onboardingState.hasCompletedWelcome {
OnboardingView {
onboardingState.completeWelcome()
}
.transition(.opacity)
}
}
.sheet(isPresented: $keepAwakePromptState.isPresented) {
KeepAwakePrompt(
onEnable: {
clockViewModel.setKeepAwakeEnabled(true)
keepAwakePromptState.dismiss()
},
onDismiss: {
keepAwakePromptState.dismiss()
}
)
}
.task {
Design.debugLog("[ContentView] App launched - initializing AlarmKit")
// Reschedule all enabled alarms with AlarmKit on app launch
await alarmViewModel.rescheduleAllAlarms()
Design.debugLog("[ContentView] AlarmKit initialization complete")
}
.onReceive(NotificationCenter.default.publisher(for: .keepAwakePromptRequested)) { _ in
guard onboardingState.hasCompletedWelcome else { return }
guard shouldShowKeepAwakePromptForTab() else { return }
keepAwakePromptState.showIfNeeded(isKeepAwakeEnabled: clockViewModel.style.keepAwake)
}
.animation(.easeInOut(duration: 0.3), value: onboardingState.hasCompletedWelcome)
}
private func shouldShowKeepAwakePromptForTab() -> Bool {
switch selectedTab {
case .clock, .alarms:
return true
case .noise, .settings:
return false
}
}
}
// MARK: - Preview
#Preview {
ContentView()
}