diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift index b795c0b..9ab4820 100644 --- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift +++ b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift @@ -18,6 +18,7 @@ public class SoundPlayer { // MARK: - Properties private var players: [String: AVAudioPlayer] = [:] + private let playersLock = NSLock() private var currentPlayer: AVAudioPlayer? public private(set) var currentSound: Sound? private var shouldResumeAfterInterruption = false @@ -27,8 +28,11 @@ public class SoundPlayer { // MARK: - Initialization private init() { setupAudioSession() - preloadSounds() setupAudioInterruptionHandling() + // Preload sounds off the main thread to avoid blocking UI during app launch + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + self?.preloadSounds() + } } deinit { @@ -58,9 +62,16 @@ public class SoundPlayer { currentSound = sound // Get or create player for this sound - guard let player = players[sound.fileName] else { + playersLock.lock() + let player = players[sound.fileName] + playersLock.unlock() + + guard let player else { + playersLock.lock() + let availableKeys = Array(players.keys) + playersLock.unlock() print("❌ Sound not preloaded: \(sound.fileName)") - print("📁 Available sounds: \(players.keys)") + print("📁 Available sounds: \(availableKeys)") // Try to load the sound dynamically as fallback guard let fileUrl = getURL(for: sound) else { @@ -73,7 +84,9 @@ public class SoundPlayer { newPlayer.numberOfLoops = AudioConstants.Playback.numberOfLoops newPlayer.volume = volumeOverride ?? AudioConstants.Volume.default newPlayer.prepareToPlay() + playersLock.lock() players[sound.fileName] = newPlayer + playersLock.unlock() currentPlayer = newPlayer let success = newPlayer.play() print("🎵 Fallback play result: \(success ? "SUCCESS" : "FAILED")") @@ -187,20 +200,27 @@ public class SoundPlayer { if settings.preloadSounds { player.prepareToPlay() } + playersLock.lock() players[sound.fileName] = player + playersLock.unlock() print("✅ Loaded: \(sound.name) (\(sound.fileName))") } catch { print("❌ Error preloading sound \(sound.fileName): \(error)") } } - print("📁 Preloading complete. Loaded \(players.count) sounds.") + playersLock.lock() + let count = players.count + playersLock.unlock() + print("📁 Preloading complete. Loaded \(count) sounds.") } private func stopAllSounds() { + playersLock.lock() for player in players.values { player.stop() } players.removeAll() + playersLock.unlock() currentPlayer = nil currentSound = nil shouldResumeAfterInterruption = false diff --git a/TheNoiseClock.xcodeproj/project.pbxproj b/TheNoiseClock.xcodeproj/project.pbxproj index 000e6ec..8954653 100644 --- a/TheNoiseClock.xcodeproj/project.pbxproj +++ b/TheNoiseClock.xcodeproj/project.pbxproj @@ -51,7 +51,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - EA384AFB2E6E6B6000CA7D50 /* TheNoiseClock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TheNoiseClock.app; sourceTree = BUILT_PRODUCTS_DIR; }; + EA384AFB2E6E6B6000CA7D50 /* The Noise Clock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "The Noise Clock.app"; sourceTree = BUILT_PRODUCTS_DIR; }; EA384B082E6E6B6100CA7D50 /* TheNoiseClockTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TheNoiseClockTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EA384B122E6E6B6100CA7D50 /* TheNoiseClockUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TheNoiseClockUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EAD6E3AF5A7F4D3DB37CF6D1 /* TheNoiseClock/Configuration/Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TheNoiseClock/Configuration/Debug.xcconfig; sourceTree = SOURCE_ROOT; }; @@ -154,7 +154,7 @@ EA384AFC2E6E6B6000CA7D50 /* Products */ = { isa = PBXGroup; children = ( - EA384AFB2E6E6B6000CA7D50 /* TheNoiseClock.app */, + EA384AFB2E6E6B6000CA7D50 /* The Noise Clock.app */, EA384B082E6E6B6100CA7D50 /* TheNoiseClockTests.xctest */, EA384B122E6E6B6100CA7D50 /* TheNoiseClockUITests.xctest */, EAF1C0DE2F3A4B5C00112234 /* TheNoiseClockWidget.appex */, @@ -197,7 +197,7 @@ EAC051B02F2E64AB007F87EA /* Bedrock */, ); productName = TheNoiseClock; - productReference = EA384AFB2E6E6B6000CA7D50 /* TheNoiseClock.app */; + productReference = EA384AFB2E6E6B6000CA7D50 /* The Noise Clock.app */; productType = "com.apple.product-type.application"; }; EA384B072E6E6B6100CA7D50 /* TheNoiseClockTests */ = { diff --git a/TheNoiseClock/App/ContentView.swift b/TheNoiseClock/App/ContentView.swift index b0fe133..4914879 100644 --- a/TheNoiseClock/App/ContentView.swift +++ b/TheNoiseClock/App/ContentView.swift @@ -45,60 +45,11 @@ struct ContentView: View { // MARK: - Body var body: some View { - ZStack { - // Main tab content - TabView(selection: $selectedTab) { - Tab("Clock", systemImage: "clock", value: AppTab.clock) { - 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) - } - } - - Tab("Alarms", systemImage: "alarm", value: AppTab.alarms) { - NavigationStack { - AlarmView(viewModel: alarmViewModel) - } - } - - Tab("Noise", systemImage: "waveform", value: AppTab.noise) { - NavigationStack { - NoiseView() - } - } - - Tab("Settings", systemImage: "gearshape", value: AppTab.settings) { - NavigationStack { - ClockSettingsView( - style: clockViewModel.style, - onCommit: { newStyle in - clockViewModel.updateStyle(newStyle) - }, - onResetOnboarding: { - onboardingState.reset() - } - ) - } - } - } - .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) - } - } - .tint(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 + Group { if !onboardingState.hasCompletedWelcome { + // Show ONLY the onboarding — no heavy app views behind it. + // This prevents ClockView, NoiseView, etc. from initializing + // and competing for the main thread during page transitions. OnboardingView { onboardingState.completeWelcome() } @@ -106,9 +57,58 @@ struct ContentView: View { insertion: .opacity, removal: .opacity.combined(with: .move(edge: .bottom)).combined(with: .scale(scale: 0.9)) )) - .zIndex(1) // Ensure it stays on top during transition + } else { + mainTabView } } + .animation(.spring(duration: 0.45, bounce: 0.2), value: onboardingState.hasCompletedWelcome) + } + + // MARK: - Main Tab View + + private var mainTabView: some View { + TabView(selection: $selectedTab) { + Tab("Clock", systemImage: "clock", value: AppTab.clock) { + NavigationStack { + ClockView(viewModel: clockViewModel, isOnClockTab: isOnClockTab) + } + } + + Tab("Alarms", systemImage: "alarm", value: AppTab.alarms) { + NavigationStack { + AlarmView(viewModel: alarmViewModel) + } + } + + Tab("Noise", systemImage: "waveform", value: AppTab.noise) { + NavigationStack { + NoiseView() + } + } + + Tab("Settings", systemImage: "gearshape", value: AppTab.settings) { + NavigationStack { + ClockSettingsView( + style: clockViewModel.style, + onCommit: { newStyle in + clockViewModel.updateStyle(newStyle) + }, + onResetOnboarding: { + onboardingState.reset() + } + ) + } + } + } + .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") + clockViewModel.setFullScreenMode(false) + } + } + .tint(AppAccent.primary) + .background(Color.Branding.primary.ignoresSafeArea()) .sheet(isPresented: $keepAwakePromptState.isPresented) { KeepAwakePrompt( onEnable: { @@ -122,10 +122,7 @@ struct ContentView: View { } .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 @@ -133,7 +130,6 @@ struct ContentView: View { guard shouldShowKeepAwakePromptForTab() else { return } keepAwakePromptState.showIfNeeded(isKeepAwakeEnabled: clockViewModel.style.keepAwake) } - .animation(.spring(duration: 0.45, bounce: 0.2), value: onboardingState.hasCompletedWelcome) } private func shouldShowKeepAwakePromptForTab() -> Bool { diff --git a/TheNoiseClock/Features/Alarms/Services/AlarmKitService.swift b/TheNoiseClock/Features/Alarms/Services/AlarmKitService.swift index 5a9e4f6..684083f 100644 --- a/TheNoiseClock/Features/Alarms/Services/AlarmKitService.swift +++ b/TheNoiseClock/Features/Alarms/Services/AlarmKitService.swift @@ -262,19 +262,6 @@ final class AlarmKitService { } } - /// Log available sound files in Library/Sounds for debugging - private func logLibrarySounds() { - let soundsDirectory = URL.libraryDirectory.appendingPathComponent("Sounds") - Design.debugLog("[alarmkit] ========== LIBRARY/SOUNDS FILES ==========") - - do { - let files = try FileManager.default.contentsOfDirectory(atPath: soundsDirectory.path) - Design.debugLog("[alarmkit] Files in Library/Sounds: \(files)") - } catch { - Design.debugLog("[alarmkit] Library/Sounds directory doesn't exist or is empty") - } - } - /// Log available alarm sounds in the bundle for debugging private func logAvailableAlarmSounds() { Design.debugLog("[alarmkit] ========== AVAILABLE ALARM SOUNDS ==========") diff --git a/TheNoiseClock/Features/Clock/Models/ClockStyle.swift b/TheNoiseClock/Features/Clock/Models/ClockStyle.swift index d73d0cc..5bcaff4 100644 --- a/TheNoiseClock/Features/Clock/Models/ClockStyle.swift +++ b/TheNoiseClock/Features/Clock/Models/ClockStyle.swift @@ -30,7 +30,7 @@ final class ClockStyle: Codable, Equatable { // MARK: - Night Mode Settings var nightModeEnabled: Bool = false - var autoNightMode: Bool = false + var autoNightMode: Bool = true var scheduledNightMode: Bool = false var nightModeStartTime: String = "22:00" // 10:00 PM var nightModeEndTime: String = "06:00" // 6:00 AM @@ -48,14 +48,13 @@ final class ClockStyle: Codable, Equatable { var showDate: Bool = true var showNextAlarm: Bool = true var showNoiseControls: Bool = true - var dateFormat: String = "d MMMM EEE" // Default: "7 September Mon" + var dateFormat: String = "d MMM yyyy" // Default: "7 Sept 2026" var clockOpacity: Double = AppConstants.Defaults.clockOpacity var overlayOpacity: Double = AppConstants.Defaults.overlayOpacity // MARK: - Display Settings var keepAwake: Bool = false // Keep screen awake in display mode var respectFocusModes: Bool = true // Respect Focus mode settings for audio - var liveActivitiesEnabled: Bool = false // Show active alarm in Dynamic Island/Lock Screen // MARK: - Cached Colors private var _cachedDigitColor: Color? @@ -92,7 +91,6 @@ final class ClockStyle: Codable, Equatable { case overlayOpacity case keepAwake case respectFocusModes - case liveActivitiesEnabled } // MARK: - Initialization @@ -146,7 +144,6 @@ final class ClockStyle: Codable, Equatable { self.overlayOpacity = try container.decodeIfPresent(Double.self, forKey: .overlayOpacity) ?? self.overlayOpacity self.keepAwake = try container.decodeIfPresent(Bool.self, forKey: .keepAwake) ?? self.keepAwake self.respectFocusModes = try container.decodeIfPresent(Bool.self, forKey: .respectFocusModes) ?? self.respectFocusModes - self.liveActivitiesEnabled = try container.decodeIfPresent(Bool.self, forKey: .liveActivitiesEnabled) ?? self.liveActivitiesEnabled clearColorCache() } @@ -182,7 +179,6 @@ final class ClockStyle: Codable, Equatable { try container.encode(overlayOpacity, forKey: .overlayOpacity) try container.encode(keepAwake, forKey: .keepAwake) try container.encode(respectFocusModes, forKey: .respectFocusModes) - try container.encode(liveActivitiesEnabled, forKey: .liveActivitiesEnabled) } // MARK: - Computed Properties @@ -476,8 +472,7 @@ final class ClockStyle: Codable, Equatable { lhs.clockOpacity == rhs.clockOpacity && lhs.overlayOpacity == rhs.overlayOpacity && lhs.keepAwake == rhs.keepAwake && - lhs.respectFocusModes == rhs.respectFocusModes && - lhs.liveActivitiesEnabled == rhs.liveActivitiesEnabled + lhs.respectFocusModes == rhs.respectFocusModes } } diff --git a/TheNoiseClock/Features/Clock/State/ClockViewModel.swift b/TheNoiseClock/Features/Clock/State/ClockViewModel.swift index 2948cc9..d407d22 100644 --- a/TheNoiseClock/Features/Clock/State/ClockViewModel.swift +++ b/TheNoiseClock/Features/Clock/State/ClockViewModel.swift @@ -124,8 +124,6 @@ final class ClockViewModel { style.digitAnimationStyle = newStyle.digitAnimationStyle style.dateFormat = newStyle.dateFormat style.respectFocusModes = newStyle.respectFocusModes - style.liveActivitiesEnabled = newStyle.liveActivitiesEnabled - saveStyle() updateTimersIfNeeded() diff --git a/TheNoiseClock/Features/Clock/Views/Components/Settings/AdvancedDisplaySection.swift b/TheNoiseClock/Features/Clock/Views/Components/Settings/AdvancedDisplaySection.swift index 9873e21..c1bf8c8 100644 --- a/TheNoiseClock/Features/Clock/Views/Components/Settings/AdvancedDisplaySection.swift +++ b/TheNoiseClock/Features/Clock/Views/Components/Settings/AdvancedDisplaySection.swift @@ -28,18 +28,6 @@ struct AdvancedDisplaySection: View { accentColor: AppAccent.primary ) - Rectangle() - .fill(AppBorder.subtle) - .frame(height: 1) - .padding(.horizontal, Design.Spacing.medium) - - SettingsToggle( - title: "Live Activities", - subtitle: "Show alarms on Lock Screen/Dynamic Island while ringing", - isOn: $style.liveActivitiesEnabled, - accentColor: AppAccent.primary - ) - if style.autoBrightness { Rectangle() .fill(AppBorder.subtle) diff --git a/TheNoiseClock/Features/Onboarding/Views/OnboardingView.swift b/TheNoiseClock/Features/Onboarding/Views/OnboardingView.swift index 53fd572..6098cc0 100644 --- a/TheNoiseClock/Features/Onboarding/Views/OnboardingView.swift +++ b/TheNoiseClock/Features/Onboarding/Views/OnboardingView.swift @@ -60,7 +60,6 @@ struct OnboardingView: View { .tag(3) } .tabViewStyle(.page(indexDisplayMode: .never)) - .animation(.easeInOut(duration: 0.3), value: currentPage) OnboardingBottomControls( currentPage: $currentPage, diff --git a/TheNoiseClock/Shared/Design/Fonts/FontUtils.swift b/TheNoiseClock/Shared/Design/Fonts/FontUtils.swift index c12dc52..7379a5a 100644 --- a/TheNoiseClock/Shared/Design/Fonts/FontUtils.swift +++ b/TheNoiseClock/Shared/Design/Fonts/FontUtils.swift @@ -152,25 +152,6 @@ struct FontUtils { ) } - private static func tightBoundingBox( - for text: String, - withFont font: UIFont - ) -> CGSize { - let attributedString = NSAttributedString( - string: text, - attributes: [.font: font] - ) - let rect = attributedString.boundingRect( - with: CGSize( - width: CGFloat.greatestFiniteMagnitude, - height: CGFloat.greatestFiniteMagnitude - ), - options: [.usesLineFragmentOrigin, .usesFontLeading], - context: nil - ) - return CGSize(width: ceil(rect.width), height: ceil(rect.height)) - } - private static func weightedFontName( name: FontFamily, weight: Font.Weight, diff --git a/TheNoiseClock/Shared/Extensions/Date+Extensions.swift b/TheNoiseClock/Shared/Extensions/Date+Extensions.swift index 5c19dd3..d247dc5 100644 --- a/TheNoiseClock/Shared/Extensions/Date+Extensions.swift +++ b/TheNoiseClock/Shared/Extensions/Date+Extensions.swift @@ -10,9 +10,9 @@ import Foundation extension Date { /// Format date for display in overlay with custom format - /// - Parameter format: Date format string (e.g., "d MMMM EEE") + /// - Parameter format: Date format string (e.g., "d MMM yyyy") /// - Returns: Formatted date string - func formattedForOverlay(format: String = "d MMMM EEE") -> String { + func formattedForOverlay(format: String = "d MMM yyyy") -> String { let formatter = DateFormatter() formatter.dateFormat = format return formatter.string(from: self) @@ -41,25 +41,13 @@ extension Date { } } - /// Get time components for alarm scheduling - /// - Returns: DateComponents with hour and minute - func timeComponents() -> DateComponents { - return Calendar.current.dateComponents([.hour, .minute], from: self) - } - - /// Check if date is today - /// - Returns: True if date is today - func isToday() -> Bool { - return Calendar.current.isDateInToday(self) - } - /// Get next occurrence of this time /// - Returns: Next occurrence of this time, or today if time hasn't passed func nextOccurrence() -> Date { let calendar = Calendar.current let now = Date() let today = calendar.startOfDay(for: now) - let timeComponents = self.timeComponents() + let timeComponents = calendar.dateComponents([.hour, .minute], from: self) guard let todayWithTime = calendar.date(byAdding: timeComponents, to: today) else { return now diff --git a/TheNoiseClock/Shared/Extensions/View+Extensions.swift b/TheNoiseClock/Shared/Extensions/View+Extensions.swift index ccd2d1d..af37f33 100644 --- a/TheNoiseClock/Shared/Extensions/View+Extensions.swift +++ b/TheNoiseClock/Shared/Extensions/View+Extensions.swift @@ -10,17 +10,6 @@ import Bedrock extension View { - /// Apply standard card styling - /// - Returns: View with card styling applied - func cardStyle() -> some View { - self - .background(AppSurface.overlay, in: RoundedRectangle(cornerRadius: Design.CornerRadius.appLarge)) - .overlay( - RoundedRectangle(cornerRadius: Design.CornerRadius.appLarge) - .stroke(AppBorder.subtle, lineWidth: Design.LineWidth.thin) - ) - } - /// Apply standard button styling /// - Parameters: /// - isEnabled: Whether the button is enabled @@ -35,28 +24,6 @@ extension View { .disabled(!isEnabled) } - - /// Apply responsive font sizing that updates on orientation and layout changes - /// - Parameters: - /// - baseSize: Base font size - /// - isPortrait: Whether in portrait orientation - /// - showSeconds: Whether seconds are displayed (for time components) - /// - showAmPm: Whether AM/PM is displayed - /// - Returns: View with responsive font sizing - func responsiveFontSize( - baseSize: CGFloat, - isPortrait: Bool, - showSeconds: Bool = false, - showAmPm: Bool = false - ) -> some View { - self.modifier(ResponsiveFontModifier( - baseSize: baseSize, - isPortrait: isPortrait, - showSeconds: showSeconds, - showAmPm: showAmPm - )) - } - /// Force view to update on orientation changes /// - Returns: View that updates on orientation changes func onOrientationChange() -> some View { @@ -81,17 +48,6 @@ extension View { } } - /// Apply standard section header styling with title - /// - Parameter title: The title text - /// - Returns: View with section header styling - func sectionHeader(title: String) -> some View { - VStack(alignment: .leading, spacing: Design.Spacing.medium) { - Text(title) - .sectionTitleStyle() - self - } - } - /// Apply standard content padding /// - Parameters: /// - horizontal: Horizontal padding amount @@ -106,22 +62,6 @@ extension View { // MARK: - View Modifiers -/// Modifier for responsive font sizing that updates on layout changes -struct ResponsiveFontModifier: ViewModifier { - let baseSize: CGFloat - let isPortrait: Bool - let showSeconds: Bool - let showAmPm: Bool - - func body(content: Content) -> some View { - content - .font(.system(size: baseSize, weight: .bold, design: .rounded)) - .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in - // Force view update on orientation change - } - } -} - /// Modifier that forces view updates on orientation changes struct OrientationChangeModifier: ViewModifier { @State private var orientation = UIDevice.current.orientation diff --git a/TheNoiseClock/Shared/Utilities/AlarmNotifications.swift b/TheNoiseClock/Shared/Utilities/AlarmNotifications.swift index 3e41436..253803c 100644 --- a/TheNoiseClock/Shared/Utilities/AlarmNotifications.swift +++ b/TheNoiseClock/Shared/Utilities/AlarmNotifications.swift @@ -7,31 +7,7 @@ import Foundation -enum AlarmNotificationConstants { - static let categoryIdentifier = "ALARM_CATEGORY" - static let snoozeActionIdentifier = "SNOOZE_ACTION" - static let stopActionIdentifier = "STOP_ACTION" -} - -enum AlarmNotificationKeys { - static let alarmId = "alarmId" - static let soundName = "soundName" - static let repeats = "repeats" - static let isSnooze = "isSnooze" - static let originalAlarmId = "originalAlarmId" - static let label = "label" - static let notificationMessage = "notificationMessage" - static let snoozeDuration = "snoozeDuration" - static let isVibrationEnabled = "isVibrationEnabled" - static let volume = "volume" - static let title = "title" - static let body = "body" -} - extension Notification.Name { - static let alarmDidFire = Notification.Name("alarmDidFire") - static let alarmDidStop = Notification.Name("alarmDidStop") - static let alarmDidSnooze = Notification.Name("alarmDidSnooze") static let keepAwakePromptRequested = Notification.Name("keepAwakePromptRequested") static let clockStyleDidUpdate = Notification.Name("clockStyleDidUpdate") } diff --git a/TheNoiseClock/Shared/Utilities/KeepAwakePromptState.swift b/TheNoiseClock/Shared/Utilities/KeepAwakePromptState.swift index b6f7df0..40ff2c7 100644 --- a/TheNoiseClock/Shared/Utilities/KeepAwakePromptState.swift +++ b/TheNoiseClock/Shared/Utilities/KeepAwakePromptState.swift @@ -23,8 +23,4 @@ final class KeepAwakePromptState { func dismiss() { isPresented = false } - - func resetSessionFlag() { - hasShownThisSession = false - } } diff --git a/TheNoiseClock/Shared/Utilities/NotificationUtils.swift b/TheNoiseClock/Shared/Utilities/NotificationUtils.swift deleted file mode 100644 index 6968ead..0000000 --- a/TheNoiseClock/Shared/Utilities/NotificationUtils.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// NotificationUtils.swift -// TheNoiseClock -// -// Created by Matt Bruce on 9/7/25. -// - -import UserNotifications -import Foundation -import Bedrock - -/// Notification helper functions -enum NotificationUtils { - - /// Request notification permissions - /// - Returns: True if permission granted - static func requestPermissions() async -> Bool { - do { - let granted = try await UNUserNotificationCenter.current().requestAuthorization( - options: [.alert, .sound, .badge] - ) - return granted - } catch { - Design.debugLog("[general] Error requesting notification permissions: \(error)") - return false - } - } - - /// Create notification content for alarm - /// - Parameters: - /// - title: Notification title - /// - body: Notification body - /// - soundName: Sound name for notification - /// - Returns: Configured notification content - static func createAlarmContent(title: String, body: String, soundName: String) -> UNMutableNotificationContent { - let content = UNMutableNotificationContent() - content.title = title - content.body = body - content.categoryIdentifier = AlarmNotificationConstants.categoryIdentifier - - if soundName == "default" { - content.sound = UNNotificationSound.default - Design.debugLog("[settings] Using default notification sound") - } else if Bundle.main.url(forResource: soundName, withExtension: nil) != nil { - // Use the sound name directly since sounds.json now references CAF files - content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: soundName)) - Design.debugLog("[settings] Using custom alarm sound: \(soundName)") - Design.debugLog("[settings] Sound file should be in main bundle: \(soundName)") - } else { - content.sound = UNNotificationSound.default - Design.debugLog("[settings] Alarm sound not found in main bundle, falling back to default: \(soundName)") - } - - return content - } - - /// Create calendar trigger for alarm - /// - Parameter date: Date for alarm - /// - Returns: Calendar notification trigger - static func createCalendarTrigger(for date: Date) -> UNCalendarNotificationTrigger { - let components = Calendar.current.dateComponents([.hour, .minute], from: date) - return UNCalendarNotificationTrigger(dateMatching: components, repeats: false) - } - - /// Schedule notification - /// - Parameters: - /// - identifier: Unique identifier for notification - /// - content: Notification content - /// - trigger: Notification trigger - /// - Returns: True if scheduled successfully - static func scheduleNotification( - identifier: String, - content: UNMutableNotificationContent, - trigger: UNNotificationTrigger - ) async -> Bool { - let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) - - do { - try await UNUserNotificationCenter.current().add(request) - return true - } catch { - Design.debugLog("[general] Error scheduling notification: \(error)") - return false - } - } - - /// Remove notification by identifier - /// - Parameter identifier: Notification identifier to remove - static func removeNotification(identifier: String) { - UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier]) - } - - /// Remove all pending notifications - static func removeAllNotifications() { - UNUserNotificationCenter.current().removeAllPendingNotificationRequests() - } -}