168 lines
5.9 KiB
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()
|
|
}
|