From 6d3837022dd8be6082f9f55eabfdba300d74a7bf Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Mon, 8 Sep 2025 16:59:46 -0500 Subject: [PATCH] Signed-off-by: Matt Bruce --- .../{NoisePlayer.swift => SoundPlayer.swift} | 8 +- ...seViewModel.swift => SoundViewModel.swift} | 22 +- PRD.md | 231 ++++++++++++------ .../Components/SoundSelectionView.swift | 6 +- .../Noise/Components/SoundCategoryView.swift | 2 +- TheNoiseClock/Views/Noise/NoiseView.swift | 2 +- 6 files changed, 170 insertions(+), 101 deletions(-) rename AudioPlaybackKit/Sources/AudioPlaybackKit/Services/{NoisePlayer.swift => SoundPlayer.swift} (98%) rename AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/{NoiseViewModel.swift => SoundViewModel.swift} (80%) diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/NoisePlayer.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift similarity index 98% rename from AudioPlaybackKit/Sources/AudioPlaybackKit/Services/NoisePlayer.swift rename to AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift index d804207..8244d1c 100644 --- a/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/NoisePlayer.swift +++ b/AudioPlaybackKit/Sources/AudioPlaybackKit/Services/SoundPlayer.swift @@ -1,5 +1,5 @@ // -// NoisePlayer.swift +// SoundPlayer.swift // AudioPlaybackKit // // Created by Matt Bruce on 9/8/25. @@ -8,13 +8,13 @@ import AVFoundation import Observation -/// Audio playback service for white noise and ambient sounds +/// Audio playback service for sounds and ambient audio @available(iOS 17.0, tvOS 17.0, *) @Observable -public class NoisePlayer { +public class SoundPlayer { // MARK: - Singleton - public static let shared = NoisePlayer() + public static let shared = SoundPlayer() // MARK: - Properties private var players: [String: AVAudioPlayer] = [:] diff --git a/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/NoiseViewModel.swift b/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/SoundViewModel.swift similarity index 80% rename from AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/NoiseViewModel.swift rename to AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/SoundViewModel.swift index df4503c..6e0fae6 100644 --- a/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/NoiseViewModel.swift +++ b/AudioPlaybackKit/Sources/AudioPlaybackKit/ViewModels/SoundViewModel.swift @@ -1,5 +1,5 @@ // -// NoiseViewModel.swift +// SoundViewModel.swift // AudioPlaybackKit // // Created by Matt Bruce on 9/8/25. @@ -8,19 +8,19 @@ import Foundation import Observation -/// ViewModel for noise/audio playback +/// ViewModel for sound/audio playback @available(iOS 17.0, tvOS 17.0, *) @Observable -public class NoiseViewModel { +public class SoundViewModel { // MARK: - Properties - private let noisePlayer: NoisePlayer + private let soundPlayer: SoundPlayer private let soundConfigurationService: SoundConfigurationService public var isPreviewing: Bool = false public var previewSound: Sound? public var isPlaying: Bool { - noisePlayer.isPlaying + soundPlayer.isPlaying } public var availableSounds: [Sound] { @@ -28,18 +28,18 @@ public class NoiseViewModel { } // MARK: - Initialization - public init(noisePlayer: NoisePlayer = NoisePlayer.shared, soundConfigurationService: SoundConfigurationService = SoundConfigurationService.shared) { - self.noisePlayer = noisePlayer + public init(soundPlayer: SoundPlayer = SoundPlayer.shared, soundConfigurationService: SoundConfigurationService = SoundConfigurationService.shared) { + self.soundPlayer = soundPlayer self.soundConfigurationService = soundConfigurationService } // MARK: - Public Interface public func playSound(_ sound: Sound) { - noisePlayer.playSound(sound) + soundPlayer.playSound(sound) } public func stopSound() { - noisePlayer.stopSound() + soundPlayer.stopSound() } public func selectSound(_ sound: Sound) { @@ -61,7 +61,7 @@ public class NoiseViewModel { isPreviewing = true // Play preview (3 seconds) - noisePlayer.playSound(sound) + soundPlayer.playSound(sound) // Auto-stop preview after 3 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { @@ -71,7 +71,7 @@ public class NoiseViewModel { public func stopPreview() { if isPreviewing { - noisePlayer.stopSound() + soundPlayer.stopSound() isPreviewing = false previewSound = nil } diff --git a/PRD.md b/PRD.md index 700e56e..a67b46c 100644 --- a/PRD.md +++ b/PRD.md @@ -63,7 +63,7 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di - **Multiple sound options**: - White Noise (`white-noise.mp3`) - Heavy Rain White Noise (`heavy-rain-white-noise.mp3`) - - Fan White Noise (`fan-white-noise-heater-303207.mp3`) + - Fan White Noise (`fan-white-noise-heater.mp3`) - **Continuous playback**: Sounds loop indefinitely - **Advanced sound selection**: Category-based grid with search and preview - **Simple controls**: Play/Stop button with visual feedback @@ -77,17 +77,18 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di - **Wake lock integration**: Prevents device sleep while audio is playing - **Bluetooth audio support**: Works with AirPods and other Bluetooth audio devices - **Responsive layout**: Optimized for both portrait and landscape orientations +- **AudioPlaybackKit integration**: Powered by reusable Swift package for audio functionality ### 6. Advanced Alarm System - **Multiple alarms**: Create and manage unlimited alarms - **Rich alarm editor**: Full-featured alarm creation and editing interface - **Time selection**: Wheel-style date picker with optimized font sizing for maximum readability -- **Dynamic alarm sounds**: Configurable alarm sounds loaded from JSON configuration +- **Dynamic alarm sounds**: Configurable alarm sounds loaded from dedicated alarm-sounds.json configuration - **Sound preview**: Play/stop functionality for testing alarm sounds before selection -- **Sound organization**: Alarm sounds organized in bundles with categories +- **Sound organization**: Alarm sounds organized in dedicated AlarmSounds.bundle with categories - **Custom labels**: User-defined alarm names and descriptions - **Repeat schedules**: Set alarms to repeat on specific weekdays or daily -- **Sound selection**: Choose from extensive system sounds with live preview +- **Sound selection**: Choose from extensive alarm sounds with live preview - **Volume control**: Adjustable alarm volume (0-100%) - **Vibration settings**: Enable/disable vibration for each alarm - **Snooze functionality**: Configurable snooze duration (5, 7, 8, 9, 10, 15, 20 minutes) @@ -98,6 +99,7 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di - **Alarm management**: Add, edit, delete, and duplicate alarms - **Next trigger preview**: Shows when the next alarm will fire - **Responsive time picker**: Font sizes adapt to available space and orientation +- **AlarmSoundService integration**: Dedicated service for alarm-specific sound management ## Advanced Clock Display Features @@ -139,6 +141,40 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di ## Technical Architecture +### Swift Package Architecture + +TheNoiseClock has been refactored to use a modular Swift Package architecture for improved code reusability and maintainability: + +#### AudioPlaybackKit Package +- **Purpose**: Reusable audio playback functionality for iOS and tvOS applications +- **Platform Support**: iOS 17.0+ and tvOS 17.0+ +- **Core Components**: + - `Sound` model: Generic sound data structure with Codable support and GUID generation + - `SoundConfiguration` models: Configuration structures for sound management + - `SoundConfigurationService`: Generic sound configuration service with JSON loading + - `NoisePlayer`: Audio playback service with background support + - `WakeLockService`: Screen wake lock management + - `NoiseViewModel`: Audio playback state management + - `AudioConstants`: Audio-related constants and configuration +- **Key Features**: + - Generic sound management (no alarm-specific functionality) + - Fatal error handling for missing configuration files + - Platform-conditional compilation for tvOS compatibility + - Modern @Observable state management + - Background audio support with interruption handling + +#### Package Integration +- **Local Package**: AudioPlaybackKit is included as a local Swift package dependency +- **Xcode Integration**: Properly configured in project.pbxproj with XCLocalSwiftPackageReference +- **Import Usage**: Main app imports AudioPlaybackKit for audio functionality +- **Separation of Concerns**: Generic audio functionality in package, app-specific logic in main app + +#### App-Specific Extensions +- **AlarmSoundService**: Dedicated service in main app for alarm-specific sound management +- **Configuration Separation**: Alarm sounds use separate alarm-sounds.json file +- **Category Constants**: Hardcoded alarm category ID to avoid magic strings +- **Service Pattern**: Extension pattern for app-specific functionality + ### Code Organization Principles **TOP PRIORITY:** The codebase must be built with the following architectural principles from the beginning: @@ -200,8 +236,17 @@ These principles are fundamental to the project's long-term success and must be - Sound name with volume control - Vibration settings - Snooze duration configuration -- **Sound**: Simple struct for noise file management +- **Sound** (AudioPlaybackKit): Generic sound data model with Codable support + - Unique GUID identifier (auto-generated) - Display name and file name + - Category and description + - Optional bundle name for organization + - Optional isDefault flag for default sound selection + - Custom Codable implementation with GUID generation +- **SoundConfiguration** (AudioPlaybackKit): Configuration structure for sound management + - Array of Sound objects + - Sound categories and audio settings + - JSON-based configuration loading - **LegacyAlarm**: Backward compatibility struct for old alarm data ### Data Persistence @@ -210,17 +255,21 @@ These principles are fundamental to the project's long-term success and must be - **Bundle resources**: Audio files stored in app bundle ### Audio System +- **AudioPlaybackKit Package**: Reusable audio functionality in Swift package - **AVFoundation**: AVAudioPlayer for noise playback - **@Observable NoisePlayer**: Modern state management with preloading - **Looping playback**: Infinite loop for ambient sounds - **Audio session management**: Proper audio session configuration with background support -- **Error handling**: Graceful handling of missing audio files +- **Error handling**: Fatal error handling for missing configuration files - **AlarmTonePlayer**: Dedicated player for alarm sound previews - **Background audio**: Continues playback when app is backgrounded - **Interruption handling**: Automatic resume after phone calls and route changes - **Wake lock integration**: Prevents device sleep during audio playback - **Focus mode awareness**: Monitors and respects Focus mode settings - **Notification compatibility**: Ensures alarms work with Focus modes enabled +- **Configuration separation**: Separate JSON files for ambient sounds (sounds.json) and alarm sounds (alarm-sounds.json) +- **Generic sound management**: AudioPlaybackKit provides generic sound functionality +- **App-specific extensions**: AlarmSoundService handles alarm-specific sound management ### Battery System - **@Observable BatteryService**: Modern state management for battery monitoring @@ -283,83 +332,95 @@ These principles are fundamental to the project's long-term success and must be ## File Structure and Organization ### Recommended File Organization -Following the separation of concerns principle, the codebase should be organized into focused, single-responsibility files: +Following the separation of concerns principle, the codebase is organized into focused, single-responsibility files with Swift Package integration: ``` TheNoiseClock/ -├── App/ -│ ├── TheNoiseClockApp.swift # App entry point and configuration -│ └── ContentView.swift # Main tab navigation coordinator -├── Core/ -│ ├── Constants/ -│ │ ├── AppConstants.swift # App-wide constants and configuration -│ │ ├── UIConstants.swift # UI-specific constants (colors, sizes, etc.) -│ │ └── AudioConstants.swift # Audio-related constants -│ ├── Extensions/ -│ │ ├── Color+Extensions.swift # Color utilities and extensions -│ │ ├── Date+Extensions.swift # Date formatting and utilities -│ │ └── View+Extensions.swift # Common view modifiers and responsive utilities -│ └── Utilities/ -│ ├── ColorUtils.swift # Color manipulation utilities -│ ├── FontUtils.swift # Font sizing, typography, and customization utilities -│ └── NotificationUtils.swift # Notification helper functions -├── Models/ -│ ├── ClockStyle.swift # Clock customization data model -│ ├── Alarm.swift # Alarm data model -│ ├── Sound.swift # Sound data model -│ └── LegacyAlarm.swift # Backward compatibility model -├── ViewModels/ -│ ├── ClockViewModel.swift # Clock display logic and state -│ ├── AlarmViewModel.swift # Alarm management logic -│ └── NoiseViewModel.swift # Audio playback state management -├── Views/ -│ ├── Clock/ -│ │ ├── ClockView.swift # Main clock display view -│ │ ├── ClockSettingsView.swift # Clock customization interface -│ │ └── Components/ -│ │ ├── TimeDisplayView.swift # Advanced segmented time display with fixed-width digits -│ │ ├── BatteryOverlayView.swift # Battery level overlay -│ │ ├── DateOverlayView.swift # Date display overlay -│ │ └── TopOverlayView.swift # Combined overlay container -│ ├── Alarms/ -│ │ ├── AlarmView.swift # Main alarm management view -│ │ ├── AddAlarmView.swift # Alarm creation interface -│ │ └── Components/ -│ │ ├── AlarmRowView.swift # Individual alarm row component -│ │ ├── TimePickerSection.swift # Time selection component -│ │ ├── TimeUntilAlarmSection.swift # Time calculation display -│ │ ├── SoundSelectionView.swift # Sound selection with preview -│ │ ├── LabelEditView.swift # Label editing interface -│ │ └── SnoozeSelectionView.swift # Snooze duration selection -│ └── Noise/ -│ ├── NoiseView.swift # Main white noise player interface -│ └── Components/ -│ ├── SoundCategoryView.swift # Advanced grid-based sound selection -│ └── SoundControlView.swift # Playback controls component -├── Services/ -│ ├── NoisePlayer.swift # Audio playback service with background support -│ ├── AlarmService.swift # Alarm management service with Focus mode integration -│ ├── NotificationService.swift # Notification handling service -│ ├── WakeLockService.swift # Screen wake lock management service -│ ├── FocusModeService.swift # Focus mode integration and notification management -│ └── BatteryService.swift # Battery monitoring and state management service -└── Resources/ - ├── sounds.json # Sound configuration and definitions - ├── Ambient.bundle/ # Ambient sound category - │ └── white-noise.mp3 - ├── Nature.bundle/ # Nature sound category - │ └── heavy-rain-white-noise.mp3 - ├── Mechanical.bundle/ # Mechanical sound category - │ └── fan-white-noise-heater.mp3 - ├── AlarmSounds.bundle/ # Alarm sound category - │ ├── digital-alarm.mp3 - │ ├── iphone-alarm.mp3 - │ ├── classic-alarm.mp3 - │ ├── beep-alarm.mp3 - │ ├── siren-alarm.mp3 - │ └── voice-wakeup.mp3 - └── Assets.xcassets/ - └── [Asset catalogs] +├── AudioPlaybackKit/ # Swift Package for reusable audio functionality +│ ├── Package.swift # Package configuration (iOS 17.0+, tvOS 17.0+) +│ └── Sources/AudioPlaybackKit/ +│ ├── Models/ +│ │ ├── Sound.swift # Generic sound data model with Codable support +│ │ └── SoundConfiguration.swift # Sound configuration models and service +│ ├── Services/ +│ │ ├── NoisePlayer.swift # Audio playback service +│ │ └── WakeLockService.swift # Screen wake lock management +│ ├── ViewModels/ +│ │ └── NoiseViewModel.swift # Audio playback state management +│ └── Constants/ +│ └── AudioConstants.swift # Audio-related constants +├── TheNoiseClock/ # Main application +│ ├── App/ +│ │ ├── TheNoiseClockApp.swift # App entry point and configuration +│ │ └── ContentView.swift # Main tab navigation coordinator +│ ├── Core/ +│ │ ├── Constants/ +│ │ │ ├── AppConstants.swift # App-wide constants and configuration +│ │ │ └── UIConstants.swift # UI-specific constants (colors, sizes, etc.) +│ │ ├── Extensions/ +│ │ │ ├── Color+Extensions.swift # Color utilities and extensions +│ │ │ ├── Date+Extensions.swift # Date formatting and utilities +│ │ │ └── View+Extensions.swift # Common view modifiers and responsive utilities +│ │ └── Utilities/ +│ │ ├── ColorUtils.swift # Color manipulation utilities +│ │ ├── FontUtils.swift # Font sizing, typography, and customization utilities +│ │ └── NotificationUtils.swift # Notification helper functions +│ ├── Models/ +│ │ ├── ClockStyle.swift # Clock customization data model +│ │ ├── Alarm.swift # Alarm data model +│ │ └── LegacyAlarm.swift # Backward compatibility model +│ ├── ViewModels/ +│ │ ├── ClockViewModel.swift # Clock display logic and state +│ │ └── AlarmViewModel.swift # Alarm management logic +│ ├── Views/ +│ │ ├── Clock/ +│ │ │ ├── ClockView.swift # Main clock display view +│ │ │ ├── ClockSettingsView.swift # Clock customization interface +│ │ │ └── Components/ +│ │ │ ├── TimeDisplayView.swift # Advanced segmented time display with fixed-width digits +│ │ │ ├── BatteryOverlayView.swift # Battery level overlay +│ │ │ ├── DateOverlayView.swift # Date display overlay +│ │ │ └── TopOverlayView.swift # Combined overlay container +│ │ ├── Alarms/ +│ │ │ ├── AlarmView.swift # Main alarm management view +│ │ │ ├── AddAlarmView.swift # Alarm creation interface +│ │ │ └── Components/ +│ │ │ ├── AlarmRowView.swift # Individual alarm row component +│ │ │ ├── TimePickerSection.swift # Time selection component +│ │ │ ├── TimeUntilAlarmSection.swift # Time calculation display +│ │ │ ├── SoundSelectionView.swift # Sound selection with preview +│ │ │ ├── LabelEditView.swift # Label editing interface +│ │ │ └── SnoozeSelectionView.swift # Snooze duration selection +│ │ └── Noise/ +│ │ ├── NoiseView.swift # Main white noise player interface +│ │ └── Components/ +│ │ ├── SoundCategoryView.swift # Advanced grid-based sound selection +│ │ └── SoundControlView.swift # Playback controls component +│ ├── Services/ +│ │ ├── AlarmService.swift # Alarm management service with Focus mode integration +│ │ ├── NotificationService.swift # Notification handling service +│ │ ├── FocusModeService.swift # Focus mode integration and notification management +│ │ ├── BatteryService.swift # Battery monitoring and state management service +│ │ └── AlarmSoundService.swift # Alarm-specific sound management service +│ └── Resources/ +│ ├── sounds.json # Ambient sound configuration and definitions +│ ├── alarm-sounds.json # Alarm sound configuration and definitions +│ ├── Ambient.bundle/ # Ambient sound category +│ │ └── white-noise.mp3 +│ ├── Nature.bundle/ # Nature sound category +│ │ └── heavy-rain-white-noise.mp3 +│ ├── Mechanical.bundle/ # Mechanical sound category +│ │ └── fan-white-noise-heater.mp3 +│ ├── AlarmSounds.bundle/ # Alarm sound category +│ │ ├── digital-alarm.caf +│ │ ├── classic-alarm.caf +│ │ ├── beep-alarm.caf +│ │ ├── siren-alarm.caf +│ │ └── buzzing-alarm.caf +│ └── Assets.xcassets/ +│ └── [Asset catalogs] +└── TheNoiseClock.xcodeproj/ # Xcode project with AudioPlaybackKit dependency + └── project.pbxproj # Project configuration with local package reference ``` ### File Naming Conventions @@ -464,6 +525,12 @@ The following changes **automatically require** PRD updates: - **iOS 26 Features**: Latest platform capabilities where available ### Dependencies +- **AudioPlaybackKit**: Local Swift package for reusable audio functionality + - iOS 17.0+ and tvOS 17.0+ support + - Generic sound management and configuration + - Audio playback with background support + - Wake lock management + - Modern @Observable state management - **SwiftUI**: Native iOS UI framework with latest features - **AVFoundation**: Audio playback with modern async patterns and background support - **UserNotifications**: Alarm notifications with rich content support @@ -514,11 +581,13 @@ The following changes **automatically require** PRD updates: ### Project Information - **Created**: September 7, 2025 - **Framework**: SwiftUI with iOS 18.0+ target (latest stable features) -- **Architecture**: Modern SwiftUI with @Observable pattern and MVVM +- **Architecture**: Modern SwiftUI with @Observable pattern, MVVM, and Swift Package modularity +- **Package Architecture**: AudioPlaybackKit Swift package for reusable audio functionality - **Testing**: Comprehensive unit and UI test targets with Swift Testing - **Version control**: Git repository with feature branch workflow - **Performance**: Optimized for battery life and smooth operation - **Modern iOS**: Uses latest iOS 18+ and iOS 26 features with Swift 6 language improvements +- **Code Reusability**: Modular Swift package architecture enables code reuse across projects ### Modern iOS Development Practices - **Swift 6**: Latest language features including strict concurrency checking diff --git a/TheNoiseClock/Views/Alarms/Components/SoundSelectionView.swift b/TheNoiseClock/Views/Alarms/Components/SoundSelectionView.swift index 36fc71f..af2bf63 100644 --- a/TheNoiseClock/Views/Alarms/Components/SoundSelectionView.swift +++ b/TheNoiseClock/Views/Alarms/Components/SoundSelectionView.swift @@ -14,7 +14,7 @@ struct SoundSelectionView: View { @Environment(\.dismiss) private var dismiss // Use shared player instance to avoid audio conflicts - private let noisePlayer = NoisePlayer.shared + private let soundPlayer = SoundPlayer.shared private let alarmSounds = AlarmSoundService.shared.getAlarmSounds().sorted { $0.name < $1.name } @State private var isPlaying = false @@ -73,13 +73,13 @@ struct SoundSelectionView: View { stopSound() // Start playing the new sound - noisePlayer.playSound(sound) + soundPlayer.playSound(sound) isPlaying = true currentlyPlayingSound = selectedSound } private func stopSound() { - noisePlayer.stopSound() + soundPlayer.stopSound() isPlaying = false currentlyPlayingSound = nil } diff --git a/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift b/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift index 21e91d7..fd561f6 100644 --- a/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift +++ b/TheNoiseClock/Views/Noise/Components/SoundCategoryView.swift @@ -16,7 +16,7 @@ struct SoundCategoryView: View { @Binding var selectedSound: Sound? @State private var selectedCategory: String = "all" @State private var searchText: String = "" - @State private var viewModel = NoiseViewModel() + @State private var viewModel = SoundViewModel() // MARK: - Computed Properties private var filteredSounds: [Sound] { diff --git a/TheNoiseClock/Views/Noise/NoiseView.swift b/TheNoiseClock/Views/Noise/NoiseView.swift index 3565894..7baf583 100644 --- a/TheNoiseClock/Views/Noise/NoiseView.swift +++ b/TheNoiseClock/Views/Noise/NoiseView.swift @@ -12,7 +12,7 @@ import AudioPlaybackKit struct NoiseView: View { // MARK: - Properties - @State private var viewModel = NoiseViewModel() + @State private var viewModel = SoundViewModel() @State private var selectedSound: Sound? { didSet { // Stop current playback when selecting a new sound