diff --git a/PRD.md b/PRD.md
index bd0919e..5c1c9a6 100644
--- a/PRD.md
+++ b/PRD.md
@@ -41,6 +41,8 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di
- **Normal mode**: Standard interface with navigation and settings
- **Display mode**: Full-screen clock activated by long-press (0.6 seconds)
- **Automatic UI hiding**: Tab bar and navigation elements hide in display mode
+- **iPad compatibility**: Uses SwiftUI's native `.toolbar(.hidden, for: .tabBar)` for proper iPad sidebar-style tab bar hiding
+- **Cross-platform support**: Works correctly on both iPhone (bottom tab bar) and iPad (top sidebar tab bar)
- **Smooth transitions**: Animated transitions between modes
- **Status bar control**: Status bar automatically hidden in full-screen mode
- **Safe area expansion**: Clock expands into tab bar area when hidden
@@ -328,6 +330,8 @@ These principles are fundamental to the project's long-term success and must be
- **Interactive controls**: Toggles, sliders, color pickers
- **Real-time updates**: Changes apply immediately
- **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
## File Structure and Organization
@@ -576,6 +580,62 @@ The following changes **automatically require** PRD updates:
- **Weather integration**: Weather-based alarm sounds
- **Health integration**: Sleep tracking integration
+## Build and Development
+
+### Terminal Build Commands
+
+The following terminal commands are used for building and testing the project. These commands have been tested and work reliably:
+
+#### Basic Build Commands
+```bash
+# Navigate to project directory
+cd /Users/mattbruce/Documents/Projects/TheNoiseClock
+
+# Build for iOS Simulator (iPad mini)
+xcodebuild -project TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=iPad mini (A17 Pro),OS=18.1' build
+
+# Build for iOS Simulator (any device)
+xcodebuild -project TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=Any iOS Simulator Device' build
+
+# Build for physical device (requires provisioning profile)
+xcodebuild -project TheNoiseClock.xcodeproj -scheme TheNoiseClock build
+```
+
+#### Error Checking Commands
+```bash
+# Check for build errors only (filtered output)
+xcodebuild -project TheNoiseClock.xcodeproj -scheme TheNoiseClock -destination 'platform=iOS Simulator,name=iPad mini (A17 Pro),OS=18.1' build 2>&1 | grep -E "(error:|warning:|failed)" | head -10
+
+# Quick syntax check for specific files
+swift -frontend -parse TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift
+swift -frontend -parse TheNoiseClock/Views/Clock/Components/TimeSegment.swift
+swift -frontend -parse TheNoiseClock/Views/Clock/Components/DigitView.swift
+```
+
+#### Available Simulators
+The following simulators are available for testing:
+- **iPad mini (A17 Pro)** - Primary testing device
+- **iPad (10th generation)**
+- **iPad Air 11-inch (M2)**
+- **iPad Air 13-inch (M2)**
+- **iPad Pro 11-inch (M4)**
+- **iPad Pro 13-inch (M4)**
+- **iPhone 16, 16 Plus, 16 Pro, 16 Pro Max**
+- **iPhone SE (3rd generation)**
+
+#### Build Troubleshooting
+1. **Provisioning Profile Errors**: Use iOS Simulator builds instead of device builds
+2. **Missing Files**: Ensure all new Swift files are added to the Xcode project target
+3. **Preview Compilation Errors**: Break down complex expressions into computed properties
+4. **Package Dependencies**: AudioPlaybackKit is included as local package dependency
+
+#### Development Workflow
+1. **Make code changes** in Xcode or via AI assistant
+2. **Test build** using terminal commands above
+3. **Fix any errors** identified in build output
+4. **Test on simulator** using Xcode or terminal build
+5. **Update PRD** if architectural changes are made
+
## Development Notes
### Project Information
diff --git a/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
index 8bc45f1..27be183 100644
--- a/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,7 +7,7 @@
TheNoiseClock.xcscheme_^#shared#^_
orderHint
- 1
+ 0
diff --git a/TheNoiseClock/Core/Extensions/View+Extensions.swift b/TheNoiseClock/Core/Extensions/View+Extensions.swift
index e7d751a..42603a7 100644
--- a/TheNoiseClock/Core/Extensions/View+Extensions.swift
+++ b/TheNoiseClock/Core/Extensions/View+Extensions.swift
@@ -34,28 +34,6 @@ extension View {
.disabled(!isEnabled)
}
- /// Hide tab bar with animation
- /// - Parameters:
- /// - hidden: Whether to hide the tab bar
- /// - animated: Whether to animate the change
- func hideTabBar(_ hidden: Bool, animated: Bool = true) {
- #if canImport(UIKit)
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
- let window = windowScene.windows.first,
- let tabBarController = window.rootViewController?.findTabBarController() else { return }
-
- let tabBar = tabBarController.tabBar
- let changes = {
- tabBar.alpha = hidden ? 0 : 1
- }
- if animated {
- UIView.animate(withDuration: AppConstants.AnimationDurations.short, animations: changes)
- } else {
- changes()
- }
- tabBar.isUserInteractionEnabled = !hidden
- #endif
- }
/// Apply responsive font sizing that updates on orientation and layout changes
/// - Parameters:
@@ -155,22 +133,3 @@ struct OrientationChangeModifier: ViewModifier {
.id(orientation.rawValue) // Force view recreation on orientation change
}
}
-
-#if canImport(UIKit)
-// Made internal (module-wide) so it can be used from other files like ClockView.swift
-extension UIViewController {
- func findTabBarController() -> UITabBarController? {
- if let tbc = self as? UITabBarController { return tbc }
- for child in children {
- if let tbc = child.findTabBarController() { return tbc }
- }
- if let presented = presentedViewController {
- return presented.findTabBarController()
- }
- if let nav = self as? UINavigationController {
- return nav.visibleViewController?.findTabBarController()
- }
- return parent?.findTabBarController()
- }
-}
-#endif
diff --git a/TheNoiseClock/Views/Clock/ClockView.swift b/TheNoiseClock/Views/Clock/ClockView.swift
index 521dd5a..35f3c0e 100644
--- a/TheNoiseClock/Views/Clock/ClockView.swift
+++ b/TheNoiseClock/Views/Clock/ClockView.swift
@@ -40,7 +40,9 @@ struct ClockView: View {
ClockSettingsView(style: viewModel.style) { newStyle in
viewModel.updateStyle(newStyle)
}
- .presentationDetents([.medium, .large])
+ .presentationDetents(UIDevice.current.userInterfaceIdiom == .pad ? [.large] : [.medium, .large])
+ .presentationDragIndicator(.visible)
+ .presentationBackgroundInteraction(.enabled)
}
.overlay {
// Toolbar overlay
@@ -51,7 +53,7 @@ struct ClockView: View {
}
.overlay {
// Tab bar management overlay
- ClockTabBarManager(isDisplayMode: viewModel.isDisplayMode, animated: true)
+ ClockTabBarManager(isDisplayMode: viewModel.isDisplayMode)
}
.overlay {
// Gesture handling overlay
@@ -67,4 +69,6 @@ struct ClockView: View {
NavigationStack {
ClockView()
}
+ .frame(width: 400, height: 600)
+ .background(Color.black)
}
diff --git a/TheNoiseClock/Views/Clock/Components/ClockDisplayContainer.swift b/TheNoiseClock/Views/Clock/Components/ClockDisplayContainer.swift
index 97d83ff..dcf26e1 100644
--- a/TheNoiseClock/Views/Clock/Components/ClockDisplayContainer.swift
+++ b/TheNoiseClock/Views/Clock/Components/ClockDisplayContainer.swift
@@ -60,4 +60,6 @@ struct ClockDisplayContainer: View {
style: ClockStyle(),
isDisplayMode: false
)
+ .frame(width: 400, height: 600)
+ .background(Color.black)
}
diff --git a/TheNoiseClock/Views/Clock/Components/ClockTabBarManager.swift b/TheNoiseClock/Views/Clock/Components/ClockTabBarManager.swift
index b016112..d42927c 100644
--- a/TheNoiseClock/Views/Clock/Components/ClockTabBarManager.swift
+++ b/TheNoiseClock/Views/Clock/Components/ClockTabBarManager.swift
@@ -8,48 +8,20 @@
import SwiftUI
/// Component that manages tab bar visibility for display mode
+/// Uses SwiftUI's native toolbar hiding for proper iPad compatibility
struct ClockTabBarManager: View {
// MARK: - Properties
let isDisplayMode: Bool
- let animated: Bool
// MARK: - Body
var body: some View {
EmptyView()
- .onAppear {
- setTabBarHidden(isDisplayMode, animated: false)
- }
- .onDisappear {
- setTabBarHidden(false, animated: false)
- }
- .onChange(of: isDisplayMode) { _, newValue in
- setTabBarHidden(newValue, animated: animated)
- }
- }
-
- // MARK: - Private Methods
- private func setTabBarHidden(_ hidden: Bool, animated: Bool) {
- #if canImport(UIKit)
- guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
- let window = windowScene.windows.first,
- let tabBarController = window.rootViewController?.findTabBarController() else { return }
-
- let tabBar = tabBarController.tabBar
- let changes = {
- tabBar.alpha = hidden ? 0 : 1
- }
- if animated {
- UIView.animate(withDuration: AppConstants.AnimationDurations.short, animations: changes)
- } else {
- changes()
- }
- tabBar.isUserInteractionEnabled = !hidden
- #endif
+ .toolbar(isDisplayMode ? .hidden : .automatic, for: .tabBar)
}
}
// MARK: - Preview
#Preview {
- ClockTabBarManager(isDisplayMode: false, animated: false)
+ ClockTabBarManager(isDisplayMode: false)
}
diff --git a/TheNoiseClock/Views/Clock/Components/DigitView.swift b/TheNoiseClock/Views/Clock/Components/DigitView.swift
new file mode 100644
index 0000000..bf29a88
--- /dev/null
+++ b/TheNoiseClock/Views/Clock/Components/DigitView.swift
@@ -0,0 +1,88 @@
+//
+// DigitView.swift
+// TheNoiseClock
+//
+// Created by Matt Bruce on 9/9/25.
+//
+
+import SwiftUI
+
+/// Component for displaying a single digit with fixed width and glow effects
+struct DigitView: View {
+ let digit: String
+ let fontSize: CGFloat
+ let opacity: Double
+ let digitColor: Color
+ let glowIntensity: Double
+ let fontFamily: String
+ let fontWeight: String
+ let fontDesign: String
+ let digitWidth: CGFloat
+ let digitHeight: CGFloat
+
+ var body: some View {
+ GeometryReader { geometry in
+ ZStack {
+ glowText
+ .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
+ mainText
+ .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
+ }
+ }
+ .frame(width: digitWidth, height: digitHeight)
+ .border(Color.blue, width: 2) // DEBUG: Blue border around individual digits
+ }
+
+ // MARK: - Computed Properties
+ private var customFont: Font {
+ FontUtils.customFont(
+ size: fontSize,
+ family: fontFamily,
+ weight: fontWeight,
+ design: fontDesign
+ )
+ }
+
+ private var glowRadius: CGFloat {
+ ColorUtils.glowRadius(intensity: glowIntensity)
+ }
+
+ private var glowOpacity: Double {
+ ColorUtils.glowOpacity(intensity: glowIntensity) * opacity
+ }
+
+ private var glowText: some View {
+ Text(digit)
+ .font(customFont)
+ .foregroundColor(digitColor)
+ .blur(radius: glowRadius)
+ .opacity(glowOpacity)
+ }
+
+ private var mainText: some View {
+ Text(digit)
+ .font(customFont)
+ .foregroundColor(digitColor)
+ .opacity(opacity)
+ }
+}
+
+// MARK: - Preview
+#Preview {
+ let digitView = DigitView(
+ digit: "8",
+ fontSize: 80,
+ opacity: 1.0,
+ digitColor: .white,
+ glowIntensity: 0.2,
+ fontFamily: "System",
+ fontWeight: "Regular",
+ fontDesign: "Default",
+ digitWidth: 60,
+ digitHeight: 100
+ )
+
+ return digitView
+ .background(Color.black)
+ .frame(width: 100, height: 120)
+}
diff --git a/TheNoiseClock/Views/Clock/Components/DotCircle.swift b/TheNoiseClock/Views/Clock/Components/DotCircle.swift
new file mode 100644
index 0000000..1d60445
--- /dev/null
+++ b/TheNoiseClock/Views/Clock/Components/DotCircle.swift
@@ -0,0 +1,78 @@
+//
+// DotCircle.swift
+// TheNoiseClock
+//
+// Created by Matt Bruce on 9/9/25.
+//
+
+import SwiftUI
+
+/// Component for displaying a single dot in the colon separator
+struct DotCircle: View {
+ let size: CGFloat
+ let opacity: Double
+ let digitColor: Color
+ let glowIntensity: Double
+ let fontWeight: String
+
+ var body: some View {
+ // Calculate size based on font weight - make dots smaller for lighter weights
+ let sizeMultiplier = FontUtils.dotSizeMultiplier(for: fontWeight)
+ let adjustedSize = size * sizeMultiplier
+
+ ZStack {
+ Circle()
+ .fill(digitColor)
+ .frame(width: adjustedSize, height: adjustedSize)
+ .blur(radius: ColorUtils.glowRadius(intensity: glowIntensity))
+ .opacity(ColorUtils.glowOpacity(intensity: glowIntensity) * opacity)
+ Circle()
+ .fill(digitColor)
+ .frame(width: adjustedSize, height: adjustedSize)
+ .opacity(opacity)
+ }
+ }
+}
+
+// MARK: - Preview
+#Preview("Small Dot") {
+ DotCircle(
+ size: 8,
+ opacity: 1.0,
+ digitColor: .white,
+ glowIntensity: 0.3,
+ fontWeight: "Light"
+ )
+ .background(Color.black)
+}
+
+#Preview("Large Dot") {
+ DotCircle(
+ size: 12,
+ opacity: 1.0,
+ digitColor: .white,
+ glowIntensity: 0.3,
+ fontWeight: "Bold"
+ )
+ .background(Color.black)
+}
+
+#Preview("Multiple Dots") {
+ HStack(spacing: 10) {
+ DotCircle(
+ size: 10,
+ opacity: 1.0,
+ digitColor: .white,
+ glowIntensity: 0.3,
+ fontWeight: "Regular"
+ )
+ DotCircle(
+ size: 10,
+ opacity: 1.0,
+ digitColor: .white,
+ glowIntensity: 0.3,
+ fontWeight: "Regular"
+ )
+ }
+ .background(Color.black)
+}
diff --git a/TheNoiseClock/Views/Clock/Components/HorizontalColon.swift b/TheNoiseClock/Views/Clock/Components/HorizontalColon.swift
new file mode 100644
index 0000000..e9cda3b
--- /dev/null
+++ b/TheNoiseClock/Views/Clock/Components/HorizontalColon.swift
@@ -0,0 +1,41 @@
+//
+// HorizontalColon.swift
+// TheNoiseClock
+//
+// Created by Matt Bruce on 9/9/25.
+//
+
+import SwiftUI
+
+/// Component for displaying horizontal colon separator (two dots side by side)
+struct HorizontalColon: View {
+ let dotDiameter: CGFloat
+ let spacing: CGFloat
+ let opacity: Double
+ let digitColor: Color
+ let glowIntensity: Double
+ let fontWeight: String
+
+ var body: some View {
+ let clamped = ColorUtils.clampOpacity(opacity)
+ HStack(spacing: spacing) {
+ DotCircle(size: dotDiameter, opacity: clamped, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
+ DotCircle(size: dotDiameter, opacity: clamped, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
+ }
+ .fixedSize(horizontal: true, vertical: true)
+ .accessibilityHidden(true)
+ }
+}
+
+// MARK: - Preview
+#Preview {
+ HorizontalColon(
+ dotDiameter: 12,
+ spacing: 8,
+ opacity: 1.0,
+ digitColor: .white,
+ glowIntensity: 0.3,
+ fontWeight: "Regular"
+ )
+ .background(Color.black)
+}
diff --git a/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift b/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift
index f94f8ab..6afc4e1 100644
--- a/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift
+++ b/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift
@@ -52,12 +52,6 @@ struct TimeDisplayView: View {
return df
}()
- private static let ampmDF: DateFormatter = {
- let df = DateFormatter()
- df.locale = Locale(identifier: "en_US_POSIX")
- df.dateFormat = "a"
- return df
- }()
// MARK: - Body
var body: some View {
@@ -89,14 +83,11 @@ struct TimeDisplayView: View {
showSeconds: showSeconds,
showAmPm: false
)
- let ampmFontSize = FontUtils.ampmFontSize(baseFontSize: baseFontSize)
// Time components
let hour = use24Hour ? Self.hour24DF.string(from: date) : Self.hour12DF.string(from: date)
let minute = Self.minuteDF.string(from: date)
let secondsText = Self.secondDF.string(from: date)
- let ampmText = Self.ampmDF.string(from: date)
- let showAMPM = false // Always use colon/dots instead of AM/PM
// Calculate sizes using fixed-width approach to prevent jumping
let digitUIFont = FontUtils.customUIFont(
@@ -105,35 +96,10 @@ struct TimeDisplayView: View {
weight: fontWeight,
design: fontDesign
)
- let ampmUIFont = FontUtils.customUIFont(
- size: ampmFontSize,
- family: fontFamily,
- weight: fontWeight,
- design: fontDesign
- )
// Calculate consistent sizes for layout
let _ = measureText("8", font: digitUIFont).height // Use 8 as reference height
- // Calculate the width of "88" for consistent sizing
- let testFont = FontUtils.customUIFont(
- size: baseFontSize,
- family: fontFamily,
- weight: fontWeight,
- design: fontDesign
- )
- let _ = calculateMaxTextWidth(font: testFont)
-
- // Calculate width and height for a single digit (using "8" as the reference)
- let singleDigitWidth = calculateMaxTextWidth(font: testFont, text: "8")
- let singleDigitHeight = calculateMaxTextHeight(font: testFont, text: "8")
-
- // All time segments use the same fixed width and height to prevent shifting
- let hourSize = CGSize(width: singleDigitWidth * 2, height: singleDigitHeight)
- let minuteSize = CGSize(width: singleDigitWidth * 2, height: singleDigitHeight)
- let secondsSize = showSeconds ? CGSize(width: singleDigitWidth * 2, height: singleDigitHeight) : .zero
- let ampmSize = showAMPM ? measureText(ampmText, font: ampmUIFont) : .zero
-
// Separators - reasonable spacing with extra padding in landscape
let dotDiameter = baseFontSize * 0.20
let hSpacing = portrait ? baseFontSize * 0.18 : baseFontSize * 0.25 // More spacing in landscape
@@ -141,17 +107,12 @@ struct TimeDisplayView: View {
let horizontalSepSize = CGSize(width: dotDiameter * 2 + hSpacing, height: dotDiameter)
let verticalSepSize = CGSize(width: dotDiameter, height: dotDiameter * 2 + vSpacing)
- // Calculate layout
+ // Calculate layout - simplified without AM/PM
let (totalWidth, totalHeight) = calculateLayoutSize(
portrait: portrait,
- hourSize: hourSize,
- minuteSize: minuteSize,
- secondsSize: secondsSize,
- ampmSize: ampmSize,
horizontalSepSize: horizontalSepSize,
verticalSepSize: verticalSepSize,
- showSeconds: showSeconds,
- showAMPM: showAMPM
+ showSeconds: showSeconds
)
// Calculate scale with maximum space utilization using full screen
@@ -173,43 +134,28 @@ struct TimeDisplayView: View {
// Time display with consistent centering and stable layout
Group {
if portrait {
- VStack(spacing: 0) {
+ VStack(alignment: .center, spacing: 0) {
TimeSegment(text: hour, fontSize: baseFontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign)
- if showAMPM {
- TimeSegment(text: ampmText, fontSize: ampmFontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign)
- } else {
- HorizontalColon(dotDiameter: dotDiameter, spacing: hSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
- }
+ HorizontalColon(dotDiameter: dotDiameter, spacing: hSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
TimeSegment(text: minute, fontSize: baseFontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign)
if showSeconds {
HorizontalColon(dotDiameter: dotDiameter, spacing: hSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
TimeSegment(text: secondsText, fontSize: baseFontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign)
- } else {
- // Invisible placeholder to maintain consistent spacing
- Spacer()
- .frame(height: baseFontSize * 0.3)
}
}
} else {
- HStack(spacing: baseFontSize * 0.035) {
+ HStack(alignment: .center, spacing: baseFontSize * 0.035) {
TimeSegment(text: hour, fontSize: baseFontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign)
- if showAMPM {
- TimeSegment(text: ampmText, fontSize: ampmFontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign)
- } else {
- VerticalColon(dotDiameter: dotDiameter, spacing: vSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
- }
+ VerticalColon(dotDiameter: dotDiameter, spacing: vSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
TimeSegment(text: minute, fontSize: baseFontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign)
if showSeconds {
VerticalColon(dotDiameter: dotDiameter, spacing: vSpacing, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
TimeSegment(text: secondsText, fontSize: baseFontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign)
- } else {
- // Invisible placeholder to maintain consistent spacing
- Spacer()
- .frame(width: baseFontSize * 0.3)
}
}
}
}
+ .border(Color.red, width: 3) // DEBUG: Red border to check positioning
.frame(width: fullScreenSize.width, height: fullScreenSize.height, alignment: .center)
.scaleEffect(finalScale, anchor: .center)
.animation(UIConstants.AnimationCurves.smooth, value: finalScale)
@@ -228,224 +174,37 @@ struct TimeDisplayView: View {
private func calculateLayoutSize(
portrait: Bool,
- hourSize: CGSize,
- minuteSize: CGSize,
- secondsSize: CGSize,
- ampmSize: CGSize,
horizontalSepSize: CGSize,
verticalSepSize: CGSize,
- showSeconds: Bool,
- showAMPM: Bool
+ showSeconds: Bool
) -> (CGFloat, CGFloat) {
+ // Simplified layout calculation without AM/PM
+ // This is just a placeholder since we're using natural sizing now
if portrait {
- var widths: [CGFloat] = [hourSize.width, minuteSize.width]
- var totalH: CGFloat = hourSize.height + minuteSize.height
-
- if showAMPM {
- widths.append(ampmSize.width)
- totalH += ampmSize.height
- } else {
- widths.append(horizontalSepSize.width)
- totalH += horizontalSepSize.height
- }
-
- if showSeconds {
- widths.append(contentsOf: [horizontalSepSize.width, secondsSize.width])
- totalH += horizontalSepSize.height + secondsSize.height
- }
-
- return (widths.max() ?? 0, totalH)
+ let totalH = horizontalSepSize.height * (showSeconds ? 2 : 1)
+ return (0, totalH) // Width will be determined by content
} else {
- var totalW: CGFloat = hourSize.width + minuteSize.width
- var heights: [CGFloat] = [hourSize.height, minuteSize.height]
-
- if showAMPM {
- totalW += ampmSize.width
- heights.append(ampmSize.height)
- } else {
- totalW += verticalSepSize.width
- heights.append(verticalSepSize.height)
- }
-
- if showSeconds {
- totalW += verticalSepSize.width + secondsSize.width
- heights.append(contentsOf: [verticalSepSize.height, secondsSize.height])
- }
-
- return (totalW, heights.max() ?? 0)
+ let totalW = verticalSepSize.width * (showSeconds ? 2 : 1)
+ return (totalW, 0) // Height will be determined by content
}
}
}
-// MARK: - Supporting Views
- // Calculate width of text for the given font - this ensures consistent width
- private func calculateMaxTextWidth(font: UIFont, text: String = "88") -> CGFloat {
- let attributes = [NSAttributedString.Key.font: font]
- let size = (text as NSString).size(withAttributes: attributes)
- return size.width
- }
-
- // Calculate height of text for the given font - this ensures consistent height
- private func calculateMaxTextHeight(font: UIFont, text: String = "8") -> CGFloat {
- let attributes = [NSAttributedString.Key.font: font]
- let size = (text as NSString).size(withAttributes: attributes)
- return size.height
- }
-
- private struct TimeSegment: View {
- let text: String
- let fontSize: CGFloat
- let opacity: Double
- let digitColor: Color
- let glowIntensity: Double
- let fontFamily: String
- let fontWeight: String
- let fontDesign: String
-
- var body: some View {
- let clamped = ColorUtils.clampOpacity(opacity)
- let font = FontUtils.customUIFont(
- size: fontSize,
- family: fontFamily,
- weight: fontWeight,
- design: fontDesign
- )
- let singleDigitWidth = calculateMaxTextWidth(font: font, text: "8")
- let singleDigitHeight = calculateMaxTextHeight(font: font, text: "8")
- let totalWidth = singleDigitWidth * CGFloat(text.count)
-
- HStack(spacing: 0) {
- ForEach(Array(text.enumerated()), id: \.offset) { index, character in
- DigitView(
- digit: String(character),
- fontSize: fontSize,
- opacity: clamped,
- digitColor: digitColor,
- glowIntensity: glowIntensity,
- fontFamily: fontFamily,
- fontWeight: fontWeight,
- fontDesign: fontDesign,
- digitWidth: singleDigitWidth,
- digitHeight: singleDigitHeight
- )
- }
- }
- .frame(width: totalWidth, alignment: .center)
- }
-
- // Calculate width of text for the given font - this ensures consistent width
- private func calculateMaxTextWidth(font: UIFont, text: String = "88") -> CGFloat {
- let attributes = [NSAttributedString.Key.font: font]
- let size = (text as NSString).size(withAttributes: attributes)
- return size.width
- }
- }
-
- private struct DigitView: View {
- let digit: String
- let fontSize: CGFloat
- let opacity: Double
- let digitColor: Color
- let glowIntensity: Double
- let fontFamily: String
- let fontWeight: String
- let fontDesign: String
- let digitWidth: CGFloat
- let digitHeight: CGFloat
-
- var body: some View {
- ZStack {
- Text(digit)
- .font(FontUtils.customFont(
- size: fontSize,
- family: fontFamily,
- weight: fontWeight,
- design: fontDesign
- ))
- .foregroundColor(digitColor)
- .blur(radius: ColorUtils.glowRadius(intensity: glowIntensity))
- .opacity(ColorUtils.glowOpacity(intensity: glowIntensity) * opacity)
- .multilineTextAlignment(.center)
- Text(digit)
- .font(FontUtils.customFont(
- size: fontSize,
- family: fontFamily,
- weight: fontWeight,
- design: fontDesign
- ))
- .foregroundColor(digitColor)
- .opacity(opacity)
- .multilineTextAlignment(.center)
- }
- .frame(width: digitWidth, height: digitHeight, alignment: .center)
- .fixedSize(horizontal: false, vertical: false)
- .lineLimit(1)
- .allowsTightening(false)
- .multilineTextAlignment(.center)
- }
- }
-
-private struct HorizontalColon: View {
- let dotDiameter: CGFloat
- let spacing: CGFloat
- let opacity: Double
- let digitColor: Color
- let glowIntensity: Double
- let fontWeight: String
-
- var body: some View {
- let clamped = ColorUtils.clampOpacity(opacity)
- HStack(spacing: spacing) {
- DotCircle(size: dotDiameter, opacity: clamped, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
- DotCircle(size: dotDiameter, opacity: clamped, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
- }
- .fixedSize(horizontal: true, vertical: true)
- .accessibilityHidden(true)
- }
-}
-
-private struct VerticalColon: View {
- let dotDiameter: CGFloat
- let spacing: CGFloat
- let opacity: Double
- let digitColor: Color
- let glowIntensity: Double
- let fontWeight: String
-
- var body: some View {
- let clamped = ColorUtils.clampOpacity(opacity)
- VStack(spacing: spacing) {
- DotCircle(size: dotDiameter, opacity: clamped, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
- DotCircle(size: dotDiameter, opacity: clamped, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
- }
- .fixedSize(horizontal: true, vertical: true)
- .accessibilityHidden(true)
- }
-}
-
-private struct DotCircle: View {
- let size: CGFloat
- let opacity: Double
- let digitColor: Color
- let glowIntensity: Double
- let fontWeight: String
-
- var body: some View {
- // Calculate size based on font weight - make dots smaller for lighter weights
- let sizeMultiplier = FontUtils.dotSizeMultiplier(for: fontWeight)
- let adjustedSize = size * sizeMultiplier
-
- ZStack {
- Circle()
- .fill(digitColor)
- .frame(width: adjustedSize, height: adjustedSize)
- .blur(radius: ColorUtils.glowRadius(intensity: glowIntensity))
- .opacity(ColorUtils.glowOpacity(intensity: glowIntensity) * opacity)
- Circle()
- .fill(digitColor)
- .frame(width: adjustedSize, height: adjustedSize)
- .opacity(opacity)
- }
- }
+// MARK: - Preview
+#Preview {
+ let style = ClockStyle()
+ return TimeDisplayView(
+ date: Date(),
+ use24Hour: style.use24Hour,
+ showSeconds: style.showSeconds,
+ digitColor: style.digitColor,
+ glowIntensity: style.glowIntensity,
+ manualScale: style.digitScale,
+ stretched: style.stretched,
+ clockOpacity: style.clockOpacity,
+ fontFamily: style.fontFamily,
+ fontWeight: style.fontWeight,
+ fontDesign: style.fontDesign
+ ) .background(Color.black)
}
diff --git a/TheNoiseClock/Views/Clock/Components/TimeSegment.swift b/TheNoiseClock/Views/Clock/Components/TimeSegment.swift
new file mode 100644
index 0000000..346e440
--- /dev/null
+++ b/TheNoiseClock/Views/Clock/Components/TimeSegment.swift
@@ -0,0 +1,94 @@
+//
+// TimeSegment.swift
+// TheNoiseClock
+//
+// Created by Matt Bruce on 9/9/25.
+//
+
+import SwiftUI
+
+/// Component for displaying a time segment (hours, minutes, seconds) with fixed-width digits
+struct TimeSegment: View {
+ let text: String
+ let fontSize: CGFloat
+ let opacity: Double
+ let digitColor: Color
+ let glowIntensity: Double
+ let fontFamily: String
+ let fontWeight: String
+ let fontDesign: String
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 0) {
+ ForEach(Array(text.enumerated()), id: \.offset) { index, character in
+ DigitView(
+ digit: String(character),
+ fontSize: fontSize,
+ opacity: clampedOpacity,
+ digitColor: digitColor,
+ glowIntensity: glowIntensity,
+ fontFamily: fontFamily,
+ fontWeight: fontWeight,
+ fontDesign: fontDesign,
+ digitWidth: singleDigitWidth,
+ digitHeight: singleDigitHeight
+ )
+ }
+ }
+ .border(Color.green, width: 2) // DEBUG: Green border around time segments
+ }
+
+ // MARK: - Computed Properties
+ private var clampedOpacity: Double {
+ ColorUtils.clampOpacity(opacity)
+ }
+
+ private var customFont: UIFont {
+ FontUtils.customUIFont(
+ size: fontSize,
+ family: fontFamily,
+ weight: fontWeight,
+ design: fontDesign
+ )
+ }
+
+ private var singleDigitWidth: CGFloat {
+ calculateMaxTextWidth(font: customFont, text: "8")
+ }
+
+ private var singleDigitHeight: CGFloat {
+ calculateMaxTextHeight(font: customFont, text: "8")
+ }
+
+ // Calculate width of text for the given font - this ensures consistent width
+ private func calculateMaxTextWidth(font: UIFont, text: String = "8") -> CGFloat {
+ let attributes = [NSAttributedString.Key.font: font]
+ let size = (text as NSString).size(withAttributes: attributes)
+ return size.width
+ }
+
+ // Calculate height of text for the given font - this ensures consistent height
+ private func calculateMaxTextHeight(font: UIFont, text: String = "8") -> CGFloat {
+ let attributes = [NSAttributedString.Key.font: font]
+ let size = (text as NSString).size(withAttributes: attributes)
+ return size.height
+ }
+}
+
+// MARK: - Preview
+#Preview {
+ let segment = TimeSegment(
+ text: "12",
+ fontSize: 80,
+ opacity: 1.0,
+ digitColor: .white,
+ glowIntensity: 0.2,
+ fontFamily: "System",
+ fontWeight: "Regular",
+ fontDesign: "Default"
+ )
+
+ return segment
+ .background(Color.black)
+ .frame(width: 200, height: 100)
+}
diff --git a/TheNoiseClock/Views/Clock/Components/VerticalColon.swift b/TheNoiseClock/Views/Clock/Components/VerticalColon.swift
new file mode 100644
index 0000000..95d9986
--- /dev/null
+++ b/TheNoiseClock/Views/Clock/Components/VerticalColon.swift
@@ -0,0 +1,41 @@
+//
+// VerticalColon.swift
+// TheNoiseClock
+//
+// Created by Matt Bruce on 9/9/25.
+//
+
+import SwiftUI
+
+/// Component for displaying vertical colon separator (two dots stacked vertically)
+struct VerticalColon: View {
+ let dotDiameter: CGFloat
+ let spacing: CGFloat
+ let opacity: Double
+ let digitColor: Color
+ let glowIntensity: Double
+ let fontWeight: String
+
+ var body: some View {
+ let clamped = ColorUtils.clampOpacity(opacity)
+ VStack(spacing: spacing) {
+ DotCircle(size: dotDiameter, opacity: clamped, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
+ DotCircle(size: dotDiameter, opacity: clamped, digitColor: digitColor, glowIntensity: glowIntensity, fontWeight: fontWeight)
+ }
+ .fixedSize(horizontal: true, vertical: true)
+ .accessibilityHidden(true)
+ }
+}
+
+// MARK: - Preview
+#Preview {
+ VerticalColon(
+ dotDiameter: 12,
+ spacing: 8,
+ opacity: 1.0,
+ digitColor: .white,
+ glowIntensity: 0.3,
+ fontWeight: "Regular"
+ )
+ .background(Color.black)
+}