diff --git a/PRD.md b/PRD.md index 79bb7d4..ac6045c 100644 --- a/PRD.md +++ b/PRD.md @@ -435,7 +435,6 @@ TheNoiseClock/ │ │ │ ├── ClockDisplayContainer.swift │ │ │ ├── ClockOverlayContainer.swift │ │ │ ├── ClockGestureHandler.swift -│ │ │ ├── ClockTabBarManager.swift │ │ │ ├── ClockToolbar.swift │ │ │ ├── FullScreenHintView.swift │ │ │ └── Settings/ diff --git a/TheNoiseClock/App/ContentView.swift b/TheNoiseClock/App/ContentView.swift index df236c1..bf99308 100644 --- a/TheNoiseClock/App/ContentView.swift +++ b/TheNoiseClock/App/ContentView.swift @@ -35,6 +35,14 @@ struct ContentView: View { @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 { @@ -51,10 +59,6 @@ struct ContentView: View { NavigationStack { AlarmView(viewModel: alarmViewModel) - .toolbar(.visible, for: .tabBar) - .onAppear { - Design.debugLog("[AlarmView] onAppear - forcing tabBar visible") - } } .tabItem { Label("Alarms", systemImage: "alarm") @@ -63,10 +67,6 @@ struct ContentView: View { NavigationStack { NoiseView() - .toolbar(.visible, for: .tabBar) - .onAppear { - Design.debugLog("[NoiseView] onAppear - forcing tabBar visible") - } } .tabItem { Label("Noise", systemImage: "waveform") @@ -83,23 +83,27 @@ struct ContentView: View { onboardingState.reset() } ) - .toolbar(.visible, for: .tabBar) - .onAppear { - Design.debugLog("[ClockSettingsView] onAppear - forcing tabBar visible") - } } .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)") + 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 diff --git a/TheNoiseClock/Features/Clock/Views/ClockView.swift b/TheNoiseClock/Features/Clock/Views/ClockView.swift index c965cef..f02d8f0 100644 --- a/TheNoiseClock/Features/Clock/Views/ClockView.swift +++ b/TheNoiseClock/Features/Clock/Views/ClockView.swift @@ -79,10 +79,7 @@ struct ClockView: View { .ignoresSafeArea() // Extend GeometryReader to full screen, we handle safe areas manually .toolbar(.hidden, for: .navigationBar) .statusBarHidden(true) - .overlay { - // Tab bar management overlay - ClockTabBarManager(isDisplayMode: viewModel.isDisplayMode) - } + // Tab bar visibility is now controlled at ContentView level to prevent race conditions .simultaneousGesture( DragGesture(minimumDistance: 0) .onChanged { _ in diff --git a/TheNoiseClock/Features/Clock/Views/Components/ClockTabBarManager.swift b/TheNoiseClock/Features/Clock/Views/Components/ClockTabBarManager.swift deleted file mode 100644 index a9d8997..0000000 --- a/TheNoiseClock/Features/Clock/Views/Components/ClockTabBarManager.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// ClockTabBarManager.swift -// TheNoiseClock -// -// Created by Matt Bruce on 9/8/25. -// - -import SwiftUI -import Bedrock - -/// Component that manages tab bar visibility for display mode -/// Uses SwiftUI's native toolbar hiding for proper iPad compatibility -struct ClockTabBarManager: View { - - // MARK: - Properties - let isDisplayMode: Bool - - // MARK: - Body - var body: some View { - EmptyView() - .toolbar(isDisplayMode ? .hidden : .automatic, for: .tabBar) - .onAppear { - Design.debugLog("[ClockTabBarManager] onAppear - isDisplayMode: \(isDisplayMode), tabBar: \(isDisplayMode ? "hidden" : "automatic")") - } - .onChange(of: isDisplayMode) { oldValue, newValue in - Design.debugLog("[ClockTabBarManager] isDisplayMode changed: \(oldValue) -> \(newValue), tabBar: \(newValue ? "hidden" : "automatic")") - } - } -} - -// MARK: - Preview -#Preview { - ClockTabBarManager(isDisplayMode: false) -}