157 lines
5.4 KiB
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()
|
|
}
|