TheNoiseClock/TheNoiseClock/App/ContentView.swift

168 lines
5.9 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
/// Single source of truth for tab bar visibility - prevents race conditions
/// Tab bar is ONLY hidden when on clock tab AND in display mode
private var shouldHideTabBar: Bool {
selectedTab == .clock && clockViewModel.isDisplayMode
}
// MARK: - Body
var body: some View {
ZStack {
// Main tab content
TabView(selection: $selectedTab) {
NavigationStack {
ClockView(viewModel: clockViewModel)
}
.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)
}
// SINGLE source of truth for tab bar visibility at TabView level
// This eliminates race conditions from multiple views competing
.toolbar(shouldHideTabBar ? .hidden : .visible, for: .tabBar)
.onChange(of: selectedTab) { oldValue, newValue in
Design.debugLog("[ContentView] Tab changed: \(oldValue) -> \(newValue), shouldHideTabBar: \(shouldHideTabBar)")
if oldValue == .clock && newValue != .clock {
Design.debugLog("[ContentView] Leaving clock tab, setting displayMode to false")
// Immediately disable display mode when leaving clock tab
// This is now a safety net - the computed property already handles visibility
clockViewModel.setDisplayMode(false)
}
}
.onChange(of: clockViewModel.isDisplayMode) { oldValue, newValue in
Design.debugLog("[ContentView] isDisplayMode changed: \(oldValue) -> \(newValue), selectedTab: \(selectedTab), shouldHideTabBar: \(shouldHideTabBar)")
}
.accentColor(AppAccent.primary)
.background(Color.Branding.primary.ignoresSafeArea())
.fullScreenCover(item: activeAlarmBinding) { alarm in
AlarmScreen(
alarm: alarm,
onSnooze: {
alarmViewModel.snoozeActiveAlarm()
},
onStop: {
alarmViewModel.stopActiveAlarm()
}
)
.interactiveDismissDisabled(true)
}
// 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()
}
)
}
.onReceive(NotificationCenter.default.publisher(for: .alarmDidFire)) { notification in
alarmViewModel.handleAlarmNotification(userInfo: notification.userInfo)
}
.onReceive(NotificationCenter.default.publisher(for: .alarmDidStop)) { _ in
alarmViewModel.stopActiveAlarm()
}
.onReceive(NotificationCenter.default.publisher(for: .alarmDidSnooze)) { _ in
alarmViewModel.stopActiveAlarm()
}
.onReceive(NotificationCenter.default.publisher(for: .keepAwakePromptRequested)) { _ in
keepAwakePromptState.showIfNeeded(isKeepAwakeEnabled: clockViewModel.style.keepAwake)
}
.animation(.easeInOut(duration: 0.3), value: onboardingState.hasCompletedWelcome)
}
private var activeAlarmBinding: Binding<Alarm?> {
Binding(
get: { alarmViewModel.activeAlarm },
set: { alarmViewModel.activeAlarm = $0 }
)
}
}
// MARK: - Preview
#Preview {
ContentView()
}