Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-02 09:36:49 -06:00
parent e4202d5853
commit 8e91eed772
4 changed files with 18 additions and 52 deletions

1
PRD.md
View File

@ -435,7 +435,6 @@ TheNoiseClock/
│ │ │ ├── ClockDisplayContainer.swift │ │ │ ├── ClockDisplayContainer.swift
│ │ │ ├── ClockOverlayContainer.swift │ │ │ ├── ClockOverlayContainer.swift
│ │ │ ├── ClockGestureHandler.swift │ │ │ ├── ClockGestureHandler.swift
│ │ │ ├── ClockTabBarManager.swift
│ │ │ ├── ClockToolbar.swift │ │ │ ├── ClockToolbar.swift
│ │ │ ├── FullScreenHintView.swift │ │ │ ├── FullScreenHintView.swift
│ │ │ └── Settings/ │ │ │ └── Settings/

View File

@ -35,6 +35,14 @@ struct ContentView: View {
@State private var onboardingState = OnboardingState(appIdentifier: "TheNoiseClock") @State private var onboardingState = OnboardingState(appIdentifier: "TheNoiseClock")
@State private var keepAwakePromptState = KeepAwakePromptState() @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 // MARK: - Body
var body: some View { var body: some View {
@ -51,10 +59,6 @@ struct ContentView: View {
NavigationStack { NavigationStack {
AlarmView(viewModel: alarmViewModel) AlarmView(viewModel: alarmViewModel)
.toolbar(.visible, for: .tabBar)
.onAppear {
Design.debugLog("[AlarmView] onAppear - forcing tabBar visible")
}
} }
.tabItem { .tabItem {
Label("Alarms", systemImage: "alarm") Label("Alarms", systemImage: "alarm")
@ -63,10 +67,6 @@ struct ContentView: View {
NavigationStack { NavigationStack {
NoiseView() NoiseView()
.toolbar(.visible, for: .tabBar)
.onAppear {
Design.debugLog("[NoiseView] onAppear - forcing tabBar visible")
}
} }
.tabItem { .tabItem {
Label("Noise", systemImage: "waveform") Label("Noise", systemImage: "waveform")
@ -83,23 +83,27 @@ struct ContentView: View {
onboardingState.reset() onboardingState.reset()
} }
) )
.toolbar(.visible, for: .tabBar)
.onAppear {
Design.debugLog("[ClockSettingsView] onAppear - forcing tabBar visible")
}
} }
.tabItem { .tabItem {
Label("Settings", systemImage: "gearshape") Label("Settings", systemImage: "gearshape")
} }
.tag(Tab.settings) .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 .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 { if oldValue == .clock && newValue != .clock {
Design.debugLog("[ContentView] Leaving clock tab, setting displayMode to false") 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) clockViewModel.setDisplayMode(false)
} }
} }
.onChange(of: clockViewModel.isDisplayMode) { oldValue, newValue in
Design.debugLog("[ContentView] isDisplayMode changed: \(oldValue) -> \(newValue), selectedTab: \(selectedTab), shouldHideTabBar: \(shouldHideTabBar)")
}
.accentColor(AppAccent.primary) .accentColor(AppAccent.primary)
.background(Color.Branding.primary.ignoresSafeArea()) .background(Color.Branding.primary.ignoresSafeArea())
.fullScreenCover(item: activeAlarmBinding) { alarm in .fullScreenCover(item: activeAlarmBinding) { alarm in

View File

@ -79,10 +79,7 @@ struct ClockView: View {
.ignoresSafeArea() // Extend GeometryReader to full screen, we handle safe areas manually .ignoresSafeArea() // Extend GeometryReader to full screen, we handle safe areas manually
.toolbar(.hidden, for: .navigationBar) .toolbar(.hidden, for: .navigationBar)
.statusBarHidden(true) .statusBarHidden(true)
.overlay { // Tab bar visibility is now controlled at ContentView level to prevent race conditions
// Tab bar management overlay
ClockTabBarManager(isDisplayMode: viewModel.isDisplayMode)
}
.simultaneousGesture( .simultaneousGesture(
DragGesture(minimumDistance: 0) DragGesture(minimumDistance: 0)
.onChanged { _ in .onChanged { _ in

View File

@ -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)
}