refactored with bedrock and organized folders

Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
Matt Bruce 2026-01-31 10:46:41 -06:00
parent 0909f93368
commit a3398f0dd0
86 changed files with 1273 additions and 576 deletions

159
PRD.md
View File

@ -223,6 +223,7 @@ These principles are fundamental to the project's long-term success and must be
### App Structure
- **Main App**: `TheNoiseClockApp.swift` - Entry point with WindowGroup
- **Branded launch**: AppLaunchView wrapped in a Color.Branding.primary ZStack
- **Tab-based navigation**: Three main tabs (Clock, Alarms, Noise)
- **SwiftUI framework**: Modern declarative UI framework with iOS 18+ and iOS 26 features
- **Dark theme**: Preferred color scheme set to dark
@ -327,19 +328,18 @@ These principles are fundamental to the project's long-term success and must be
### Visual Design
- **Rounded corners**: Modern iOS design language
- **Modern animations**: iOS 18+ smooth and bouncy animations
- **Color consistency**: Blue accent color throughout
- **Color consistency**: Bedrock theme with branded surfaces and accents
- **Branded launch**: AppLaunchView with matching launch screen background
- **Accessibility**: Proper labels and hidden decorative elements
- **Form-based layouts**: Organized sections for settings and alarm editing
- **Interactive controls**: Toggles, sliders, color pickers, and date pickers
- **Card-based layouts**: Bedrock settings cards for grouping controls
- **Interactive controls**: Bedrock toggles, sliders, pickers, and color pickers
### Settings Interface
- **Form-based layout**: Organized sections for different setting categories
- **Interactive controls**: Toggles, sliders, color pickers, enum-based pickers
- **Bedrock layout**: Section headers with card-based grouping
- **Interactive controls**: SettingsToggle, SettingsSlider, menu pickers, color pickers
- **Type-safe font selection**: FontFamily.allCases, Font.Weight.allCases, Font.Design.allCases pickers
- **Real-time updates**: Changes apply immediately with live preview
- **Sheet presentation**: Modal settings with detents
- **iPad optimization**: Settings sheet opens at full size (.large) on iPad for better usability
- **iPhone compatibility**: Settings sheet uses medium/large detents on iPhone for optimal space usage
- **Sheet presentation**: Full-screen settings sheet for uninterrupted editing
- **Enum-based architecture**: Type-safe picker selections eliminate string-based errors
## File Structure and Organization
@ -349,6 +349,7 @@ Following the separation of concerns principle, the codebase is organized into f
```
TheNoiseClock/
├── README.md # App Store copy + project overview
├── AudioPlaybackKit/ # Swift Package for reusable audio functionality
│ ├── Package.swift # Package configuration (iOS 17.0+, tvOS 17.0+)
│ └── Sources/AudioPlaybackKit/
@ -366,69 +367,101 @@ TheNoiseClock/
│ ├── App/
│ │ ├── TheNoiseClockApp.swift # App entry point and configuration
│ │ └── ContentView.swift # Main tab navigation coordinator
│ ├── Core/
│ │ ├── Constants/
│ ├── Configuration/
│ │ ├── AppIdentifiers.swift # xcconfig-backed identifiers
│ │ ├── Base.xcconfig # Company/app identifiers
│ │ ├── Debug.xcconfig # Debug settings
│ │ └── Release.xcconfig # Release settings
│ ├── Shared/
│ │ ├── BrandingConfig.swift # Bedrock branding + launch config
│ │ ├── Design/
│ │ │ ├── AppConstants.swift # App-wide constants and configuration
│ │ │ └── UIConstants.swift # UI-specific constants (colors, sizes, etc.)
│ │ │ ├── UIConstants.swift # UI-specific constants (colors, sizes, etc.)
│ │ │ └── Fonts/
│ │ │ ├── Font.Design.swift
│ │ │ ├── Font.Weight.swift
│ │ │ ├── FontFamily.swift
│ │ │ └── FontUtils.swift
│ │ ├── Theme/
│ │ │ └── NoiseClockTheme.swift # Bedrock app color theme
│ │ ├── Extensions/
│ │ │ ├── Color+Extensions.swift # Color utilities and extensions
│ │ │ ├── Date+Extensions.swift # Date formatting and utilities
│ │ │ └── View+Extensions.swift # Common view modifiers and responsive utilities
│ │ ├── Models/
│ │ │ └── SoundCategory.swift # Shared sound category definitions
│ │ └── Utilities/
│ │ ├── ColorUtils.swift # Color manipulation utilities
│ │ ├── FontUtils.swift # Font sizing, typography, and customization utilities
│ │ ├── DebugLogger.swift # Debug logging helper
│ │ └── 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/
│ ├── Features/
│ │ ├── Clock/
│ │ │ ├── ClockView.swift # Main clock display view
│ │ │ ├── ClockSettingsView.swift # Clock customization interface
│ │ │ └── Components/
│ │ │ ├── TimeDisplayView.swift # Advanced segmented time display with dynamic font sizing
│ │ │ ├── TimeSegment.swift # Individual time segment with enum-based font properties
│ │ │ ├── DigitView.swift # Single digit display with binary search font sizing
│ │ │ ├── ColonView.swift # Unified colon separator (horizontal/vertical)
│ │ │ ├── DotCircle.swift # Individual dot component for colons
│ │ │ ├── BatteryOverlayView.swift # Battery level overlay
│ │ │ ├── DateOverlayView.swift # Date display overlay
│ │ │ ├── TopOverlayView.swift # Combined overlay container
│ │ │ └── Settings/
│ │ │ ├── BasicAppearanceSection.swift
│ │ │ ├── BasicDisplaySection.swift
│ │ │ ├── AdvancedAppearanceSection.swift
│ │ │ ├── AdvancedDisplaySection.swift
│ │ │ ├── FontSection.swift
│ │ │ ├── NightModeSection.swift
│ │ │ ├── OverlaySection.swift
│ │ │ └── TimePickerView.swift
│ │ │ ├── Models/
│ │ │ │ └── ClockStyle.swift
│ │ │ ├── State/
│ │ │ │ └── ClockViewModel.swift
│ │ │ ├── Services/
│ │ │ │ ├── AmbientLightService.swift
│ │ │ │ └── BatteryService.swift
│ │ │ └── Views/
│ │ │ ├── ClockView.swift
│ │ │ ├── ClockSettingsView.swift
│ │ │ └── Components/
│ │ │ ├── TimeDisplayView.swift
│ │ │ ├── TimeSegment.swift
│ │ │ ├── DigitView.swift
│ │ │ ├── ColonView.swift
│ │ │ ├── DotCircle.swift
│ │ │ ├── BatteryOverlayView.swift
│ │ │ ├── DateOverlayView.swift
│ │ │ ├── TopOverlayView.swift
│ │ │ ├── ClockDisplayContainer.swift
│ │ │ ├── ClockOverlayContainer.swift
│ │ │ ├── ClockGestureHandler.swift
│ │ │ ├── ClockTabBarManager.swift
│ │ │ ├── ClockToolbar.swift
│ │ │ ├── FullScreenHintView.swift
│ │ │ └── Settings/
│ │ │ ├── BasicAppearanceSection.swift
│ │ │ ├── BasicDisplaySection.swift
│ │ │ ├── AdvancedAppearanceSection.swift
│ │ │ ├── AdvancedDisplaySection.swift
│ │ │ ├── FontSection.swift
│ │ │ ├── NightModeSection.swift
│ │ │ ├── OverlaySection.swift
│ │ │ └── TimePickerView.swift
│ │ ├── 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
│ │ │ ├── Models/
│ │ │ │ └── Alarm.swift
│ │ │ ├── State/
│ │ │ │ └── AlarmViewModel.swift
│ │ │ ├── Services/
│ │ │ │ ├── AlarmService.swift
│ │ │ │ ├── AlarmSoundService.swift
│ │ │ │ ├── FocusModeService.swift
│ │ │ │ ├── NotificationService.swift
│ │ │ │ └── NotificationDelegate.swift
│ │ │ └── Views/
│ │ │ ├── AlarmView.swift
│ │ │ ├── AddAlarmView.swift
│ │ │ ├── EditAlarmView.swift
│ │ │ └── Components/
│ │ │ ├── AlarmRowView.swift
│ │ │ ├── EmptyAlarmsView.swift
│ │ │ ├── LabelEditView.swift
│ │ │ ├── NotificationMessageEditView.swift
│ │ │ ├── SnoozeSelectionView.swift
│ │ │ ├── SoundSelectionView.swift
│ │ │ ├── TimePickerSection.swift
│ │ │ └── TimeUntilAlarmSection.swift
│ │ └── 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
│ │ └── Views/
│ │ ├── NoiseView.swift
│ │ └── Components/
│ │ ├── SoundCategoryView.swift
│ │ └── SoundControlView.swift
│ └── Resources/
│ ├── LaunchScreen.storyboard # Branded native launch screen
│ ├── sounds.json # Ambient sound configuration and definitions
│ ├── alarm-sounds.json # Alarm sound configuration and definitions
│ ├── Ambient.bundle/ # Ambient sound category
@ -557,6 +590,10 @@ The following changes **automatically require** PRD updates:
- Audio playback with background support
- Wake lock management
- Modern @Observable state management
- **Bedrock**: Local Swift package for design system and settings UI
- App-wide theming with color providers
- Branded launch experience and icon tooling
- Reusable settings components (cards, toggles, sliders)
- **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
@ -566,6 +603,10 @@ The following changes **automatically require** PRD updates:
- **UIKit**: UIFont integration for precise text measurement and font customization
- **UIApplication**: Screen wake lock management and idle timer control
### Build Configuration
- **xcconfig**: Centralized identifiers (bundle IDs, team ID, app groups)
- **Info.plist bridge**: App identifiers exposed via AppIdentifiers.swift
### Advanced Font and Typography Utilities
- **FontFamily enum**: Type-safe font family selection with allCases support
- **Font.Weight extension**: Enhanced weight enum with allCases and uiFontWeight properties

136
README.md Normal file
View File

@ -0,0 +1,136 @@
# TheNoiseClock
TheNoiseClock is a SwiftUI iOS app that blends a bold, full-screen digital clock with white noise playback and a rich alarm system. It is optimized for iOS 18+ with Swift 6, built on a modular architecture, and styled with the Bedrock design system.
---
## App Store Information
### App Name
TheNoiseClock
### Subtitle (30 chars max)
Clock, noise, and alarms
### Promotional Text (170 chars max)
A clean, full-screen clock with ambient soundscapes and a powerful alarm editor. Customize colors, fonts, and glow for a distraction-free display.
### Keywords (100 chars max)
clock,alarm,white noise,ambient,sleep,timer,focus,night mode,bedside
### Description
TheNoiseClock is a distraction-free digital clock with built-in white noise and a fully featured alarm system. Designed for late-night focus, bedside use, and calm environments, it fills the screen with bold, stable digits and lets you tune the look and sound to your space.
**Clock**
- Full-screen, orientation-aware clock with optional seconds and AM/PM
- Fixed-width digits prevent layout "jumping"
- Auto-fit sizing and manual scale control
- Custom fonts, weights, and designs with live preview
- Glow and opacity controls for low-light comfort
**White Noise**
- Multiple ambient categories and curated sound packs
- Seamless looping with background audio support
- Quick preview on long-press, instant play/stop controls
**Alarms**
- Unlimited alarms with labels, repeat schedules, and snooze options
- Alarm sound library with preview
- Vibration and volume controls per alarm
- Focus-mode aware scheduling
**Display Mode**
- Long-press to enter immersive display mode
- Auto-hides navigation and status bar
- Optional wake-lock to keep the screen on
### What's New
- Branded launch experience with Bedrock theming
- Redesigned settings interface with cards, toggles, and sliders
- Centralized build identifiers via xcconfig
---
## Features
- Real-time digital clock with full customization
- Full-screen display mode and Dynamic Island awareness
- White noise playback with categories and previews
- Rich alarm editor with scheduling and snooze controls
- Bedrock-based theming and branded launch
- iPhone and iPad support with adaptive layouts
---
## Requirements
- iOS 18.0+
- Xcode 16+
- Swift 6
---
## Build & Run
Open `TheNoiseClock.xcodeproj` and run the `TheNoiseClock` scheme.
### Terminal Build
```bash
cd /Users/mattbruce/Documents/Projects/iPhone/TheNoiseClock
xcodebuild -project TheNoiseClock/TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=iPad mini (A17 Pro),OS=18.1' build
```
---
## Branding & Theming
The app uses Bedrock for theme tokens, settings UI, and the launch experience.
- Theme: `TheNoiseClock/Shared/Theme/NoiseClockTheme.swift`
- Branding config: `TheNoiseClock/Shared/BrandingConfig.swift`
- Launch screen: `TheNoiseClock/Resources/LaunchScreen.storyboard`
To generate an app icon in DEBUG:
1. Open Clock Settings -> Debug
2. Tap **Icon Generator**
3. Export `AppIcon.png` and drop it into `Assets.xcassets/AppIcon`
---
## Build Configuration (xcconfig)
Identifiers are centralized in xcconfig files:
- `TheNoiseClock/Configuration/Base.xcconfig`
- `TheNoiseClock/Configuration/Debug.xcconfig`
- `TheNoiseClock/Configuration/Release.xcconfig`
Update `COMPANY_IDENTIFIER` and `DEVELOPMENT_TEAM` in `Base.xcconfig` to migrate to a new account.
Swift access is provided via:
`TheNoiseClock/Configuration/AppIdentifiers.swift`
---
## Architecture
TheNoiseClock follows a clean, modular structure:
```
TheNoiseClock/
├── AudioPlaybackKit/ # Reusable audio package
├── TheNoiseClock/ # Main app target
│ ├── App/ # App entry, TabView
│ ├── Configuration/ # xcconfig + AppIdentifiers
│ ├── Features/ # Clock, Alarms, Noise
│ ├── Shared/ # Theme, branding, utilities
│ └── Resources/ # Bundles, JSON, assets
└── TheNoiseClock.xcodeproj/
```
---
## Support & Contact
For feedback, feature requests, or support:
support@thenoiseclock.app

View File

@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
EA384E832E6F806200CA7D50 /* AudioPlaybackKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */; };
EAC051B12F2E64AB007F87EA /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EAC051B02F2E64AB007F87EA /* Bedrock */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -31,6 +32,9 @@
EA384AFB2E6E6B6000CA7D50 /* TheNoiseClock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TheNoiseClock.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; };
EAD6E3AE5A7F4D3DB37CF6D1 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TheNoiseClock/Configuration/Base.xcconfig; sourceTree = SOURCE_ROOT; };
EAD6E3AF5A7F4D3DB37CF6D1 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TheNoiseClock/Configuration/Debug.xcconfig; sourceTree = SOURCE_ROOT; };
EAD6E3B05A7F4D3DB37CF6D1 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TheNoiseClock/Configuration/Release.xcconfig; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@ -70,6 +74,7 @@
buildActionMask = 2147483647;
files = (
EA384E832E6F806200CA7D50 /* AudioPlaybackKit in Frameworks */,
EAC051B12F2E64AB007F87EA /* Bedrock in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -131,6 +136,7 @@
name = TheNoiseClock;
packageProductDependencies = (
EA384D3D2E6F554D00CA7D50 /* AudioPlaybackKit */,
EAC051B02F2E64AB007F87EA /* Bedrock */,
);
productName = TheNoiseClock;
productReference = EA384AFB2E6E6B6000CA7D50 /* TheNoiseClock.app */;
@ -216,6 +222,7 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */,
EAC051AF2F2E64AB007F87EA /* XCLocalSwiftPackageReference "../Bedrock" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = EA384AFC2E6E6B6000CA7D50 /* Products */;
@ -293,6 +300,7 @@
/* Begin XCBuildConfiguration section */
EA384B1A2E6E6B6100CA7D50 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = EAD6E3AF5A7F4D3DB37CF6D1 /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@ -326,7 +334,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -357,6 +365,7 @@
};
EA384B1B2E6E6B6100CA7D50 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = EAD6E3B05A7F4D3DB37CF6D1 /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@ -390,7 +399,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -414,18 +423,19 @@
};
EA384B1D2E6E6B6100CA7D50 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = EAD6E3AF5A7F4D3DB37CF6D1 /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TheNoiseClock/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 18;
LD_RUNPATH_SEARCH_PATHS = (
@ -433,7 +443,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.TheNoiseClock;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -447,18 +457,19 @@
};
EA384B1E2E6E6B6100CA7D50 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = EAD6E3B05A7F4D3DB37CF6D1 /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TheNoiseClock/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 18;
LD_RUNPATH_SEARCH_PATHS = (
@ -466,7 +477,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.TheNoiseClock;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -480,15 +491,16 @@
};
EA384B202E6E6B6100CA7D50 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = EAD6E3AF5A7F4D3DB37CF6D1 /* Debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.TheNoiseClockTests;
PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -502,15 +514,16 @@
};
EA384B212E6E6B6100CA7D50 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = EAD6E3B05A7F4D3DB37CF6D1 /* Release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.TheNoiseClockTests;
PRODUCT_BUNDLE_IDENTIFIER = "$(TESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -524,13 +537,14 @@
};
EA384B232E6E6B6100CA7D50 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = EAD6E3AF5A7F4D3DB37CF6D1 /* Debug.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.TheNoiseClockUITests;
PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -544,13 +558,14 @@
};
EA384B242E6E6B6100CA7D50 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = EAD6E3B05A7F4D3DB37CF6D1 /* Release.xcconfig */;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.TheNoiseClockUITests;
PRODUCT_BUNDLE_IDENTIFIER = "$(UITESTS_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@ -608,6 +623,10 @@
isa = XCLocalSwiftPackageReference;
relativePath = AudioPlaybackKit;
};
EAC051AF2F2E64AB007F87EA /* XCLocalSwiftPackageReference "../Bedrock" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../Bedrock;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -616,6 +635,10 @@
package = EA384D3C2E6F554D00CA7D50 /* XCLocalSwiftPackageReference "AudioPlaybackKit" */;
productName = AudioPlaybackKit;
};
EAC051B02F2E64AB007F87EA /* Bedrock */ = {
isa = XCSwiftPackageProductDependency;
productName = Bedrock;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = EA384AF32E6E6B6000CA7D50 /* Project object */;

View File

@ -7,7 +7,7 @@
<key>TheNoiseClock.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>2</integer>
</dict>
</dict>
</dict>

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import Bedrock
/// Main tab navigation coordinator
struct ContentView: View {
@ -34,8 +35,8 @@ struct ContentView: View {
Label("Noise", systemImage: "waveform")
}
}
.accentColor(UIConstants.Colors.accentColor)
.preferredColorScheme(.dark)
.accentColor(AppAccent.primary)
.background(Color.Branding.primary.ignoresSafeArea())
}
}

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import Bedrock
/// App entry point and configuration
@main
@ -20,7 +21,15 @@ struct TheNoiseClockApp: App {
// MARK: - Body
var body: some Scene {
WindowGroup {
ContentView()
ZStack {
Color.Branding.primary
.ignoresSafeArea()
AppLaunchView(config: .noiseClock) {
ContentView()
}
}
.preferredColorScheme(.dark)
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,48 +1,9 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"filename" : "AppIcon-1024px.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],

View File

@ -0,0 +1,45 @@
//
// AppIdentifiers.swift
// TheNoiseClock
//
// Created by Matt Bruce on 1/31/26.
//
import Foundation
enum AppIdentifiers {
static let appGroupIdentifier: String = {
Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String
?? "group.com.mbrucedogs.TheNoiseClock"
}()
static let cloudKitContainerIdentifier: String = {
Bundle.main.object(forInfoDictionaryKey: "CloudKitContainerIdentifier") as? String
?? "iCloud.com.mbrucedogs.TheNoiseClock"
}()
static let appClipDomain: String = {
Bundle.main.object(forInfoDictionaryKey: "AppClipDomain") as? String
?? "thenoiseclock.app"
}()
static var bundleIdentifier: String {
Bundle.main.bundleIdentifier ?? "com.mbrucedogs.TheNoiseClock"
}
static var watchBundleIdentifier: String {
"\(bundleIdentifier).watchkitapp"
}
static var appClipBundleIdentifier: String {
"\(bundleIdentifier).Clip"
}
static var widgetBundleIdentifier: String {
"\(bundleIdentifier).Widget"
}
static func appClipURL(recordName: String) -> URL? {
URL(string: "https://\(appClipDomain)/appclip?id=\(recordName)")
}
}

View File

@ -0,0 +1,27 @@
// Base.xcconfig - Source of truth for all identifiers
// MIGRATION: Update COMPANY_IDENTIFIER and DEVELOPMENT_TEAM below
// =============================================================================
// COMPANY IDENTIFIER - CHANGE THIS FOR MIGRATION
// =============================================================================
COMPANY_IDENTIFIER = com.mbrucedogs
APP_NAME = TheNoiseClock
DEVELOPMENT_TEAM = 6R7KLBPBLZ
// =============================================================================
// DERIVED IDENTIFIERS - DO NOT EDIT
// =============================================================================
APP_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)
WATCH_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).watchkitapp
APPCLIP_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Clip
WIDGET_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Widget
INTENT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Intent
TESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)Tests
UITESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)UITests
APP_GROUP_IDENTIFIER = group.$(COMPANY_IDENTIFIER).$(APP_NAME)
CLOUDKIT_CONTAINER_IDENTIFIER = iCloud.$(COMPANY_IDENTIFIER).$(APP_NAME)
APPCLIP_DOMAIN = thenoiseclock.app

View File

@ -0,0 +1,6 @@
// Debug.xcconfig
#include "Base.xcconfig"
// Debug-specific settings
SWIFT_OPTIMIZATION_LEVEL = -Onone
ENABLE_TESTABILITY = YES

View File

@ -0,0 +1,6 @@
// Release.xcconfig
#include "Base.xcconfig"
// Release-specific settings
SWIFT_OPTIMIZATION_LEVEL = -O
SWIFT_COMPILATION_MODE = wholemodule

View File

@ -127,4 +127,4 @@ struct AddAlarmView: View {
private func getSoundDisplayName(_ fileName: String) -> String {
return AlarmSoundService.shared.getSoundDisplayName(fileName)
}
}
}

View File

@ -0,0 +1,129 @@
//
// ClockSettingsView.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
import Bedrock
/// Settings interface for clock customization
struct ClockSettingsView: View {
// MARK: - Properties
@State private var style: ClockStyle
let onCommit: (ClockStyle) -> Void
@State private var digitColor: Color = .white
@State private var backgroundColor: Color = .black
@State private var showAdvancedSettings = false
@Environment(\.dismiss) private var dismiss
// MARK: - Init
init(style: ClockStyle, onCommit: @escaping (ClockStyle) -> Void) {
self._style = State(initialValue: style)
self.onCommit = onCommit
}
// MARK: - Body
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: Design.Spacing.xxLarge) {
BasicAppearanceSection(
style: $style,
digitColor: $digitColor,
backgroundColor: $backgroundColor
)
BasicDisplaySection(style: $style)
if showAdvancedSettings {
AdvancedAppearanceSection(style: $style)
FontSection(style: $style)
NightModeSection(style: $style)
OverlaySection(style: $style)
AdvancedDisplaySection(style: $style)
}
SettingsSectionHeader(
title: "Advanced",
systemImage: "gearshape",
accentColor: AppAccent.primary
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsToggle(
title: "Show Advanced Settings",
subtitle: "Reveal additional customization options",
isOn: $showAdvancedSettings,
accentColor: AppAccent.primary
)
}
#if DEBUG
SettingsSectionHeader(
title: "Debug",
systemImage: "ant.fill",
accentColor: AppStatus.error
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsNavigationRow(
title: "Icon Generator",
subtitle: "Generate and save app icon",
backgroundColor: AppSurface.primary
) {
IconGeneratorView(config: .noiseClock, appName: "TheNoiseClock")
}
SettingsNavigationRow(
title: "Branding Preview",
subtitle: "Preview icon and launch screen",
backgroundColor: AppSurface.primary
) {
BrandingPreviewView(
iconConfig: .noiseClock,
launchConfig: .noiseClock,
appName: "TheNoiseClock"
)
}
}
#endif
}
.padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.large)
.padding(.bottom, Design.Spacing.xxxLarge)
}
.background(AppSurface.primary)
.navigationTitle("Clock Settings")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") { dismiss() }
.foregroundStyle(AppAccent.primary)
}
}
.onAppear {
digitColor = Color(hex: style.digitColorHex) ?? .white
backgroundColor = Color(hex: style.backgroundHex) ?? .black
}
.onDisappear {
onCommit(style)
}
}
}
}
// MARK: - Preview
#Preview {
ClockSettingsView(
style: ClockStyle(),
onCommit: { _ in }
)
}

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import Bedrock
/// Main clock display view with settings and display mode
struct ClockView: View {
@ -46,9 +47,10 @@ struct ClockView: View {
ClockSettingsView(style: viewModel.style) { newStyle in
viewModel.updateStyle(newStyle)
}
.presentationDetents(UIDevice.current.userInterfaceIdiom == .pad ? [.large] : [.medium, .large])
.presentationDragIndicator(.visible)
.presentationDetents([.large])
.presentationDragIndicator(.hidden)
.presentationBackgroundInteraction(.enabled)
.presentationBackground(AppSurface.overlay)
}
.overlay {
// Toolbar overlay

View File

@ -0,0 +1,75 @@
//
// AdvancedAppearanceSection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
import Bedrock
struct AdvancedAppearanceSection: View {
@Binding var style: ClockStyle
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Advanced Appearance",
systemImage: "sparkles",
accentColor: AppAccent.primary
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsToggle(
title: "Randomize Color",
subtitle: "Shift the color every minute",
isOn: $style.randomizeColor,
accentColor: AppAccent.primary
)
SettingsToggle(
title: "Stretched (AutoFit)",
subtitle: "Fit the clock to available space",
isOn: $style.stretched,
accentColor: AppAccent.primary
)
if !style.stretched {
SettingsSlider(
title: "Size",
subtitle: "Manual scaling when autofit is off",
value: $style.digitScale,
in: 0.0...1.0,
step: 0.01,
format: SliderFormat.percentage,
accentColor: AppAccent.primary
)
}
SettingsSlider(
title: "Glow",
subtitle: "Adjust the glow intensity",
value: $style.glowIntensity,
in: 0.0...1.0,
step: 0.01,
format: SliderFormat.percentage,
accentColor: AppAccent.primary
)
SettingsSlider(
title: "Clock Opacity",
subtitle: "Set the clock transparency",
value: $style.clockOpacity,
in: 0.0...1.0,
step: 0.01,
format: SliderFormat.percentage,
accentColor: AppAccent.primary
)
}
Text("Fine-tune the visual appearance of your clock.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}
}
}

View File

@ -0,0 +1,67 @@
//
// AdvancedDisplaySection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
import Bedrock
struct AdvancedDisplaySection: View {
@Binding var style: ClockStyle
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Advanced Display",
systemImage: "eye",
accentColor: AppAccent.primary
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsToggle(
title: "Keep Awake",
subtitle: "Prevent sleep in display mode",
isOn: $style.keepAwake,
accentColor: AppAccent.primary
)
if style.autoBrightness {
HStack {
Text("Current Brightness")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Spacer()
Text("\(Int(style.effectiveBrightness * 100))%")
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
}
}
}
Text("Advanced display and system integration settings.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
SettingsSectionHeader(
title: "Focus Modes",
systemImage: "moon.zzz.fill",
accentColor: AppAccent.primary
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsToggle(
title: "Respect Focus Modes",
subtitle: "Follow Do Not Disturb rules",
isOn: $style.respectFocusModes,
accentColor: AppAccent.primary
)
}
Text("Control how the app behaves when Focus modes are active.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}
}
}

View File

@ -0,0 +1,106 @@
//
// BasicAppearanceSection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
import Bedrock
struct BasicAppearanceSection: View {
@Binding var style: ClockStyle
@Binding var digitColor: Color
@Binding var backgroundColor: Color
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Colors",
systemImage: "paintpalette.fill",
accentColor: AppAccent.primary
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("Color Theme")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Picker("Color Theme", selection: $style.selectedColorTheme) {
ForEach(ClockStyle.availableColorThemes(), id: \.0) { theme in
HStack {
Circle()
.fill(themeColor(for: theme.0))
.frame(width: 20, height: 20)
Text(theme.1)
}
.tag(theme.0)
}
}
.pickerStyle(.menu)
.onChange(of: style.selectedColorTheme) { _, newTheme in
if newTheme != "Custom" {
style.applyColorTheme(newTheme)
digitColor = Color(hex: style.digitColorHex) ?? .white
backgroundColor = Color(hex: style.backgroundHex) ?? .black
}
}
}
if style.selectedColorTheme == "Custom" {
ColorPicker("Digit Color", selection: $digitColor, supportsOpacity: false)
.foregroundStyle(AppTextColors.primary)
ColorPicker("Background Color", selection: $backgroundColor, supportsOpacity: true)
.foregroundStyle(AppTextColors.primary)
}
}
}
Text("Choose your favorite color theme or create a custom look.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}
.onChange(of: backgroundColor) { _, newValue in
style.backgroundHex = newValue.toHex() ?? AppConstants.Defaults.backgroundColorHex
style.selectedColorTheme = "Custom"
style.clearColorCache()
}
.onChange(of: digitColor) { _, newValue in
style.digitColorHex = newValue.toHex() ?? AppConstants.Defaults.digitColorHex
style.selectedColorTheme = "Custom"
style.clearColorCache()
}
}
/// Get the color for a theme
private func themeColor(for theme: String) -> Color {
switch theme {
case "Custom":
return .gray
case "Night":
return .white
case "Day":
return .black
case "Red":
return .red
case "Orange":
return .orange
case "Yellow":
return .yellow
case "Green":
return .green
case "Blue":
return .blue
case "Purple":
return .purple
case "Pink":
return .pink
case "White":
return .white
default:
return .gray
}
}
}

View File

@ -0,0 +1,68 @@
//
// BasicDisplaySection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
import Bedrock
struct BasicDisplaySection: View {
@Binding var style: ClockStyle
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Display",
systemImage: "display",
accentColor: AppAccent.primary
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsToggle(
title: "24Hour Format",
subtitle: "Use military time",
isOn: $style.use24Hour,
accentColor: AppAccent.primary
)
SettingsToggle(
title: "Show Seconds",
subtitle: "Display seconds in the clock",
isOn: $style.showSeconds,
accentColor: AppAccent.primary
)
if !style.use24Hour {
SettingsToggle(
title: "Show AM/PM",
subtitle: "Add an AM/PM indicator",
isOn: $style.showAmPm,
accentColor: AppAccent.primary
)
}
SettingsToggle(
title: "Auto Brightness",
subtitle: "Adapt brightness to ambient light",
isOn: $style.autoBrightness,
accentColor: AppAccent.primary
)
if UIDevice.current.orientation.isPortrait || UIDevice.current.orientation == .unknown {
SettingsToggle(
title: "Horizontal Mode",
subtitle: "Force a wide layout in portrait",
isOn: $style.forceHorizontalMode,
accentColor: AppAccent.primary
)
}
}
Text("Basic display settings for your clock.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}
}
}

View File

@ -0,0 +1,105 @@
//
// FontSection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
import Bedrock
struct FontSection: View {
@Binding var style: ClockStyle
// Computed property for available weights based on selected font
private var availableWeights: [Font.Weight] {
if style.fontFamily == .system {
return Font.Weight.allCases
} else {
return style.fontFamily.fontWeights
}
}
// Computed property for sorted font families (System first, then alphabetical)
private var sortedFontFamilies: [FontFamily] {
let allFamilies = FontFamily.allCases
let systemFamily = allFamilies.filter { $0 == .system }
let otherFamilies = allFamilies.filter { $0 != .system }.sorted { $0.rawValue < $1.rawValue }
return systemFamily + otherFamilies
}
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Font",
systemImage: "textformat",
accentColor: AppAccent.primary
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("Family")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Picker("Family", selection: $style.fontFamily) {
ForEach(sortedFontFamilies, id: \.self) { family in
Text(family.rawValue).tag(family)
}
}
.pickerStyle(.menu)
.onChange(of: style.fontFamily) { _, newFamily in
if newFamily != .system {
style.fontDesign = .default
}
let weights = newFamily == .system ? Font.Weight.allCases : newFamily.fontWeights
if !weights.contains(style.fontWeight) {
style.fontWeight = weights.first ?? .regular
}
}
}
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("Weight")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Picker("Weight", selection: $style.fontWeight) {
ForEach(availableWeights, id: \.self) { weight in
Text(weight.rawValue).tag(weight)
}
}
.pickerStyle(.menu)
}
if style.fontFamily == .system {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("Design")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Picker("Design", selection: $style.fontDesign) {
ForEach(Font.Design.allCases, id: \.self) { design in
Text(design.rawValue).tag(design)
}
}
.pickerStyle(.menu)
}
}
HStack {
Text("Preview")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.secondary)
Spacer()
Text("12:34")
.font(FontUtils.createFont(name: style.fontFamily, weight: style.fontWeight, design: style.fontDesign, size: 24))
.foregroundStyle(AppTextColors.primary)
}
}
}
}
}
}

View File

@ -0,0 +1,91 @@
//
// NightModeSection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
import Bedrock
struct NightModeSection: View {
@Binding var style: ClockStyle
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Night Mode",
systemImage: "moon.stars.fill",
accentColor: AppAccent.primary
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsToggle(
title: "Enable Night Mode",
subtitle: "Use a red clock for low light",
isOn: $style.nightModeEnabled,
accentColor: AppAccent.primary
)
SettingsToggle(
title: "Auto Night Mode",
subtitle: "Trigger based on ambient light",
isOn: $style.autoNightMode,
accentColor: AppAccent.primary
)
if style.autoNightMode {
SettingsSlider(
title: "Light Threshold",
subtitle: "Lower values activate sooner",
value: $style.ambientLightThreshold,
in: 0.1...0.8,
step: 0.01,
format: SliderFormat.percentage,
accentColor: AppAccent.primary
)
}
SettingsToggle(
title: "Scheduled Night Mode",
subtitle: "Enable on a daily schedule",
isOn: $style.scheduledNightMode,
accentColor: AppAccent.primary
)
if style.scheduledNightMode {
HStack {
Text("Start Time")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Spacer()
TimePickerView(timeString: $style.nightModeStartTime)
}
HStack {
Text("End Time")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Spacer()
TimePickerView(timeString: $style.nightModeEndTime)
}
}
if style.isNightModeActive {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "moon.fill")
.foregroundStyle(AppStatus.error)
Text("Night Mode Active")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppStatus.error)
Spacer()
}
}
}
Text("Night mode displays the clock in red to reduce eye strain in low light environments.")
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
}
}
}

View File

@ -0,0 +1,66 @@
//
// OverlaySection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
import Bedrock
struct OverlaySection: View {
@Binding var style: ClockStyle
private let dateFormats = Date.availableDateFormats()
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Overlays",
systemImage: "rectangle.on.rectangle.angled",
accentColor: AppAccent.primary
)
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsSlider(
title: "Overlay Opacity",
subtitle: "Adjust battery and date visibility",
value: $style.overlayOpacity,
in: 0.0...1.0,
step: 0.01,
format: SliderFormat.percentage,
accentColor: AppAccent.primary
)
SettingsToggle(
title: "Battery Level",
subtitle: "Show battery percentage",
isOn: $style.showBattery,
accentColor: AppAccent.primary
)
SettingsToggle(
title: "Date",
subtitle: "Display the current date",
isOn: $style.showDate,
accentColor: AppAccent.primary
)
if style.showDate {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("Date Format")
.font(.subheadline.weight(.medium))
.foregroundStyle(AppTextColors.primary)
Picker("Date Format", selection: $style.dateFormat) {
ForEach(dateFormats, id: \.1) { format in
Text(format.0).tag(format.1)
}
}
.pickerStyle(.menu)
}
}
}
}
}
}

View File

@ -6,6 +6,7 @@
//
import SwiftUI
import Bedrock
struct TimePickerView: View {
@Binding var timeString: String
@ -14,6 +15,7 @@ struct TimePickerView: View {
var body: some View {
DatePicker("", selection: $selectedTime, displayedComponents: .hourAndMinute)
.labelsHidden()
.tint(AppAccent.primary)
.onAppear {
updateSelectedTimeFromString()
}

View File

@ -6,5 +6,11 @@
<array>
<string>audio</string>
</array>
<key>AppGroupIdentifier</key>
<string>$(APP_GROUP_IDENTIFIER)</string>
<key>CloudKitContainerIdentifier</key>
<string>$(CLOUDKIT_CONTAINER_IDENTIFIER)</string>
<key>AppClipDomain</key>
<string>$(APPCLIP_DOMAIN)</string>
</dict>
</plist>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.08" green="0.10" blue="0.16" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,53 @@
//
// BrandingConfig.swift
// TheNoiseClock
//
// Created by Matt Bruce on 1/31/26.
//
import SwiftUI
import Bedrock
// MARK: - App Branding Colors
extension Color {
enum Branding {
static let primary = Color(red: 0.08, green: 0.10, blue: 0.16)
static let secondary = Color(red: 0.03, green: 0.05, blue: 0.10)
static let accent = Color.white
}
}
// MARK: - App Icon Configuration
extension AppIconConfig {
static let noiseClock = AppIconConfig(
title: "NOISE CLOCK",
subtitle: nil,
iconSymbol: "waveform",
primaryColor: Color.Branding.primary,
secondaryColor: Color.Branding.secondary,
accentColor: Color.Branding.accent
)
}
// MARK: - Launch Screen Configuration
extension LaunchScreenConfig {
static let noiseClock = LaunchScreenConfig(
title: "NOISE CLOCK",
tagline: "Sleep in sound",
iconSymbols: ["waveform", "clock.fill"],
cornerSymbol: "sparkle",
decorativeSymbol: "circle.fill",
patternStyle: .radial,
layoutStyle: .iconAboveTitle,
primaryColor: Color.Branding.primary,
secondaryColor: Color.Branding.secondary,
accentColor: Color.Branding.accent,
titleColor: .white,
iconSpacing: 10,
animationDuration: 0.6,
showLoadingIndicator: false
)
}

View File

@ -307,4 +307,3 @@ private extension Font.Design {
}
}
}

View File

@ -0,0 +1,100 @@
//
// NoiseClockTheme.swift
// TheNoiseClock
//
// Created by Matt Bruce on 1/31/26.
//
import SwiftUI
import Bedrock
// MARK: - NoiseClock Surface Colors
public enum NoiseClockSurfaceColors: SurfaceColorProvider {
public static let primary = Color(red: 0.06, green: 0.08, blue: 0.12)
public static let secondary = Color(red: 0.09, green: 0.11, blue: 0.18)
public static let tertiary = Color(red: 0.12, green: 0.15, blue: 0.22)
public static let overlay = Color(red: 0.08, green: 0.10, blue: 0.16)
public static let card = Color(red: 0.10, green: 0.13, blue: 0.20)
public static let groupedFill = Color(red: 0.09, green: 0.12, blue: 0.18)
public static let sectionFill = Color(red: 0.12, green: 0.16, blue: 0.24)
}
// MARK: - NoiseClock Text Colors
public enum NoiseClockTextColors: TextColorProvider {
public static let primary = Color.white
public static let secondary = Color.white.opacity(Design.Opacity.accent)
public static let tertiary = Color.white.opacity(Design.Opacity.medium)
public static let disabled = Color.white.opacity(Design.Opacity.light)
public static let placeholder = Color.white.opacity(Design.Opacity.overlay)
public static let inverse = Color.black
}
// MARK: - NoiseClock Accent Colors
public enum NoiseClockAccentColors: AccentColorProvider {
public static let primary = Color(red: 0.45, green: 0.75, blue: 1.00)
public static let light = Color(red: 0.65, green: 0.85, blue: 1.00)
public static let dark = Color(red: 0.25, green: 0.55, blue: 0.90)
public static let secondary = Color(red: 0.90, green: 0.95, blue: 1.00)
}
// MARK: - NoiseClock Button Colors
public enum NoiseClockButtonColors: ButtonColorProvider {
public static let primaryLight = Color(red: 0.55, green: 0.80, blue: 1.00)
public static let primaryDark = Color(red: 0.20, green: 0.50, blue: 0.85)
public static let secondary = Color.white.opacity(Design.Opacity.subtle)
public static let destructive = Color.red.opacity(Design.Opacity.heavy)
public static let cancelText = Color.white.opacity(Design.Opacity.strong)
}
// MARK: - NoiseClock Status Colors
public enum NoiseClockStatusColors: StatusColorProvider {
public static let success = Color(red: 0.20, green: 0.80, blue: 0.45)
public static let warning = Color(red: 1.00, green: 0.75, blue: 0.20)
public static let error = Color(red: 0.90, green: 0.30, blue: 0.30)
public static let info = Color(red: 0.50, green: 0.70, blue: 0.95)
}
// MARK: - NoiseClock Border Colors
public enum NoiseClockBorderColors: BorderColorProvider {
public static let subtle = Color.white.opacity(Design.Opacity.subtle)
public static let standard = Color.white.opacity(Design.Opacity.hint)
public static let emphasized = Color.white.opacity(Design.Opacity.light)
public static let selected = NoiseClockAccentColors.primary.opacity(Design.Opacity.medium)
}
// MARK: - NoiseClock Interactive Colors
public enum NoiseClockInteractiveColors: InteractiveColorProvider {
public static let selected = NoiseClockAccentColors.primary.opacity(Design.Opacity.selection)
public static let hover = Color.white.opacity(Design.Opacity.subtle)
public static let pressed = Color.white.opacity(Design.Opacity.hint)
public static let focus = NoiseClockAccentColors.light
}
// MARK: - NoiseClock Theme
public enum NoiseClockTheme: AppColorTheme {
public typealias Surface = NoiseClockSurfaceColors
public typealias Text = NoiseClockTextColors
public typealias Accent = NoiseClockAccentColors
public typealias Button = NoiseClockButtonColors
public typealias Status = NoiseClockStatusColors
public typealias Border = NoiseClockBorderColors
public typealias Interactive = NoiseClockInteractiveColors
}
// MARK: - Convenience Typealiases
typealias AppSurface = NoiseClockSurfaceColors
typealias AppTextColors = NoiseClockTextColors
typealias AppAccent = NoiseClockAccentColors
typealias AppButtonColors = NoiseClockButtonColors
typealias AppStatus = NoiseClockStatusColors
typealias AppBorder = NoiseClockBorderColors
typealias AppInteractive = NoiseClockInteractiveColors

View File

@ -1,83 +0,0 @@
//
// ClockSettingsView.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
/// Settings interface for clock customization
struct ClockSettingsView: View {
// MARK: - Properties
@State private var style: ClockStyle
let onCommit: (ClockStyle) -> Void
@State private var digitColor: Color = .white
@State private var backgroundColor: Color = .black
@State private var showAdvancedSettings = false
// MARK: - Init
init(style: ClockStyle, onCommit: @escaping (ClockStyle) -> Void) {
self._style = State(initialValue: style)
self.onCommit = onCommit
}
// MARK: - Body
var body: some View {
NavigationView {
Form {
// BASIC SETTINGS - Most commonly used
BasicAppearanceSection(
style: $style,
digitColor: $digitColor,
backgroundColor: $backgroundColor,
onCommit: onCommit
)
BasicDisplaySection(style: $style)
// ADVANCED SETTINGS - Toggle to show/hide
if showAdvancedSettings {
AdvancedAppearanceSection(
style: $style,
digitColor: $digitColor,
backgroundColor: $backgroundColor,
onCommit: onCommit
)
FontSection(style: $style)
NightModeSection(style: $style)
OverlaySection(style: $style)
AdvancedDisplaySection(style: $style)
}
// TOGGLE FOR ADVANCED SETTINGS
Section {
Toggle("Show Advanced Settings", isOn: $showAdvancedSettings)
}
}
.navigationTitle("Clock Settings")
.navigationBarTitleDisplayMode(.inline)
.onAppear {
digitColor = Color(hex: style.digitColorHex) ?? .white
backgroundColor = Color(hex: style.backgroundHex) ?? .black
}
.onDisappear {
onCommit(style)
}
}
}
}
// MARK: - Preview
#Preview {
ClockSettingsView(
style: ClockStyle(),
onCommit: { _ in }
)
}

View File

@ -1,46 +0,0 @@
//
// AdvancedAppearanceSection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
struct AdvancedAppearanceSection: View {
@Binding var style: ClockStyle
@Binding var digitColor: Color
@Binding var backgroundColor: Color
let onCommit: (ClockStyle) -> Void
var body: some View {
Section(header: Text("Advanced Appearance"), footer: Text("Fine-tune the visual appearance of your clock.")) {
Toggle("Randomize Color (every minute)", isOn: $style.randomizeColor)
Toggle("Stretched (auto-fit)", isOn: $style.stretched)
if !style.stretched {
HStack {
Text("Size")
Slider(value: $style.digitScale, in: 0.0...1.0)
Text("\(Int((min(max(style.digitScale, 0.0), 1.0)) * 100))%")
.frame(width: 50, alignment: .trailing)
}
}
HStack {
Text("Glow")
Slider(value: $style.glowIntensity, in: 0...1)
Text("\(Int(style.glowIntensity * 100))%")
.frame(width: 50, alignment: .trailing)
}
HStack {
Text("Clock Opacity")
Slider(value: $style.clockOpacity, in: 0.0...1.0)
Text("\(Int(style.clockOpacity * 100))%")
.frame(width: 50, alignment: .trailing)
}
}
}
}

View File

@ -1,31 +0,0 @@
//
// AdvancedDisplaySection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
struct AdvancedDisplaySection: View {
@Binding var style: ClockStyle
var body: some View {
Section(header: Text("Advanced Display"), footer: Text("Advanced display and system integration settings.")) {
Toggle("Keep Awake in Display Mode", isOn: $style.keepAwake)
if style.autoBrightness {
HStack {
Text("Current Brightness")
Spacer()
Text("\(Int(style.effectiveBrightness * 100))%")
.foregroundColor(.secondary)
}
}
}
Section(header: Text("Focus Modes"), footer: Text("Control how the app behaves when Focus modes (Do Not Disturb) are active.")) {
Toggle("Respect Focus Modes", isOn: $style.respectFocusModes)
}
}
}

View File

@ -1,86 +0,0 @@
//
// BasicAppearanceSection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
struct BasicAppearanceSection: View {
@Binding var style: ClockStyle
@Binding var digitColor: Color
@Binding var backgroundColor: Color
let onCommit: (ClockStyle) -> Void
var body: some View {
Section(header: Text("Colors"), footer: Text("Choose your favorite color theme or create a custom look.")) {
// Color Theme Picker
Picker("Color Theme", selection: $style.selectedColorTheme) {
ForEach(ClockStyle.availableColorThemes(), id: \.0) { theme in
HStack {
Circle()
.fill(themeColor(for: theme.0))
.frame(width: 20, height: 20)
Text(theme.1)
}
.tag(theme.0)
}
}
.pickerStyle(.menu)
.onChange(of: style.selectedColorTheme) { _, newTheme in
if newTheme != "Custom" {
style.applyColorTheme(newTheme)
digitColor = Color(hex: style.digitColorHex) ?? .white
backgroundColor = Color(hex: style.backgroundHex) ?? .black
}
}
// Custom color pickers (only show if Custom is selected)
if style.selectedColorTheme == "Custom" {
ColorPicker("Digit Color", selection: $digitColor, supportsOpacity: false)
ColorPicker("Background Color", selection: $backgroundColor, supportsOpacity: true)
}
}
.onChange(of: backgroundColor) { _, newValue in
style.backgroundHex = newValue.toHex() ?? AppConstants.Defaults.backgroundColorHex
style.selectedColorTheme = "Custom"
style.clearColorCache()
}
.onChange(of: digitColor) { _, newValue in
style.digitColorHex = newValue.toHex() ?? AppConstants.Defaults.digitColorHex
style.selectedColorTheme = "Custom"
style.clearColorCache()
}
}
/// Get the color for a theme
private func themeColor(for theme: String) -> Color {
switch theme {
case "Custom":
return .gray
case "Night":
return .white
case "Day":
return .black
case "Red":
return .red
case "Orange":
return .orange
case "Yellow":
return .yellow
case "Green":
return .green
case "Blue":
return .blue
case "Purple":
return .purple
case "Pink":
return .pink
case "White":
return .white
default:
return .gray
}
}
}

View File

@ -1,30 +0,0 @@
//
// BasicDisplaySection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
struct BasicDisplaySection: View {
@Binding var style: ClockStyle
var body: some View {
Section(header: Text("Display"), footer: Text("Basic display settings for your clock.")) {
Toggle("24Hour Format", isOn: $style.use24Hour)
Toggle("Show Seconds", isOn: $style.showSeconds)
if !style.use24Hour {
Toggle("Show AM/PM", isOn: $style.showAmPm)
}
Toggle("Auto Brightness", isOn: $style.autoBrightness)
// Only show horizontal mode option in portrait orientation
if UIDevice.current.orientation.isPortrait || UIDevice.current.orientation == .unknown {
Toggle("Horizontal Mode", isOn: $style.forceHorizontalMode)
}
}
}
}

View File

@ -1,81 +0,0 @@
//
// FontSection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
struct FontSection: View {
@Binding var style: ClockStyle
// Computed property for available weights based on selected font
private var availableWeights: [Font.Weight] {
if style.fontFamily == .system {
return Font.Weight.allCases
} else {
return style.fontFamily.fontWeights
}
}
// Computed property for sorted font families (System first, then alphabetical)
private var sortedFontFamilies: [FontFamily] {
let allFamilies = FontFamily.allCases
let systemFamily = allFamilies.filter { $0 == .system }
let otherFamilies = allFamilies.filter { $0 != .system }.sorted { $0.rawValue < $1.rawValue }
return systemFamily + otherFamilies
}
var body: some View {
Section(header: Text("Font")) {
// Font Family
Picker("Family", selection: $style.fontFamily) {
ForEach(sortedFontFamilies, id: \.self) { family in
Text(family.rawValue).tag(family)
}
}
.pickerStyle(.menu)
.onChange(of: style.fontFamily) { _, newFamily in
// Auto-set design to default for non-system fonts
if newFamily != .system {
style.fontDesign = .default
}
// Auto-set weight to first available weight if current weight is not available
let weights = newFamily == .system ? Font.Weight.allCases : newFamily.fontWeights
if !weights.contains(style.fontWeight) {
style.fontWeight = weights.first ?? .regular
}
}
// Font Weight - show available weights for selected font
Picker("Weight", selection: $style.fontWeight) {
ForEach(availableWeights, id: \.self) { weight in
Text(weight.rawValue).tag(weight)
}
}
.pickerStyle(.menu)
// Font Design - only show for system font
if style.fontFamily == .system {
Picker("Design", selection: $style.fontDesign) {
ForEach(Font.Design.allCases, id: \.self) { design in
Text(design.rawValue).tag(design)
}
}
.pickerStyle(.menu)
}
// Font Preview
HStack {
Text("Preview:")
.foregroundColor(.secondary)
Spacer()
Text("12:34")
.font(FontUtils.createFont(name: style.fontFamily, weight: style.fontWeight, design: style.fontDesign, size: 24))
.foregroundColor(.primary)
}
}
}
}

View File

@ -1,56 +0,0 @@
//
// NightModeSection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
struct NightModeSection: View {
@Binding var style: ClockStyle
var body: some View {
Section(header: Text("Night Mode"), footer: Text("Night mode displays the clock in red to reduce eye strain in low light environments.")) {
Toggle("Enable Night Mode", isOn: $style.nightModeEnabled)
Toggle("Auto Night Mode", isOn: $style.autoNightMode)
if style.autoNightMode {
HStack {
Text("Light Threshold")
Spacer()
Slider(value: $style.ambientLightThreshold, in: 0.1...0.8)
Text("\(Int(style.ambientLightThreshold * 100))%")
.frame(width: 50, alignment: .trailing)
}
}
Toggle("Scheduled Night Mode", isOn: $style.scheduledNightMode)
if style.scheduledNightMode {
HStack {
Text("Start Time")
Spacer()
TimePickerView(timeString: $style.nightModeStartTime)
}
HStack {
Text("End Time")
Spacer()
TimePickerView(timeString: $style.nightModeEndTime)
}
}
if style.isNightModeActive {
HStack {
Image(systemName: "moon.fill")
.foregroundColor(.red)
Text("Night Mode Active")
.foregroundColor(.red)
Spacer()
}
}
}
}
}

View File

@ -1,37 +0,0 @@
//
// OverlaySection.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
struct OverlaySection: View {
@Binding var style: ClockStyle
private let dateFormats = Date.availableDateFormats()
var body: some View {
Section(header: Text("Overlays")) {
HStack {
Text("Overlay Opacity")
Slider(value: $style.overlayOpacity, in: 0.0...1.0)
Text("\(Int(style.overlayOpacity * 100))%")
.frame(width: 50, alignment: .trailing)
}
Toggle("Battery Level", isOn: $style.showBattery)
Toggle("Date", isOn: $style.showDate)
if style.showDate {
Picker("Date Format", selection: $style.dateFormat) {
ForEach(dateFormats, id: \.1) { format in
Text(format.0).tag(format.1)
}
}
.pickerStyle(.menu)
}
}
}
}