207 lines
9.5 KiB
Swift
207 lines
9.5 KiB
Swift
//
|
|
// TimeDisplayView.swift
|
|
// TheNoiseClock
|
|
//
|
|
// Created by Matt Bruce on 9/7/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
/// Component for displaying segmented time with customizable formatting
|
|
struct TimeDisplayView: View {
|
|
|
|
// MARK: - Properties
|
|
let date: Date
|
|
let use24Hour: Bool
|
|
let showSeconds: Bool
|
|
let digitColor: Color
|
|
let glowIntensity: Double
|
|
let manualScale: Double
|
|
let stretched: Bool
|
|
let clockOpacity: Double
|
|
let fontFamily: String
|
|
let fontWeight: String
|
|
let fontDesign: String
|
|
|
|
// MARK: - Formatters
|
|
private static let hour24DF: DateFormatter = {
|
|
let df = DateFormatter()
|
|
df.locale = Locale(identifier: "en_US_POSIX")
|
|
df.dateFormat = "HH"
|
|
return df
|
|
}()
|
|
|
|
private static let hour12DF: DateFormatter = {
|
|
let df = DateFormatter()
|
|
df.locale = Locale(identifier: "en_US_POSIX")
|
|
df.dateFormat = "h"
|
|
return df
|
|
}()
|
|
|
|
private static let minuteDF: DateFormatter = {
|
|
let df = DateFormatter()
|
|
df.locale = Locale(identifier: "en_US_POSIX")
|
|
df.dateFormat = "mm"
|
|
return df
|
|
}()
|
|
|
|
private static let secondDF: DateFormatter = {
|
|
let df = DateFormatter()
|
|
df.locale = Locale(identifier: "en_US_POSIX")
|
|
df.dateFormat = "ss"
|
|
return df
|
|
}()
|
|
|
|
|
|
// MARK: - Body
|
|
var body: some View {
|
|
GeometryReader { proxy in
|
|
let size = proxy.size
|
|
let portrait = size.height >= size.width
|
|
|
|
// Get safe area information for font sizing to avoid Dynamic Island overlap
|
|
let safeAreaInsets = proxy.safeAreaInsets
|
|
let safeWidth = size.width - safeAreaInsets.leading - safeAreaInsets.trailing // availableWidth
|
|
let safeHeight = size.height - safeAreaInsets.top - safeAreaInsets.bottom // availableHeight
|
|
let fullScreenSize = CGSize(width: safeWidth, height: safeHeight)
|
|
|
|
// Use optimal font sizing that maximizes space usage with full screen
|
|
let baseFontSize = stretched ?
|
|
FontUtils.maximumStretchedFontSize(
|
|
containerWidth: fullScreenSize.width,
|
|
containerHeight: fullScreenSize.height,
|
|
isPortrait: portrait,
|
|
showSeconds: showSeconds
|
|
) :
|
|
FontUtils.optimalFontSize(
|
|
containerWidth: fullScreenSize.width,
|
|
containerHeight: fullScreenSize.height,
|
|
isPortrait: portrait,
|
|
showSeconds: showSeconds
|
|
)
|
|
|
|
// 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)
|
|
|
|
// Calculate sizes using fixed-width approach to prevent jumping
|
|
let digitUIFont = FontUtils.customUIFont(
|
|
size: baseFontSize,
|
|
family: fontFamily,
|
|
weight: fontWeight,
|
|
design: fontDesign
|
|
)
|
|
|
|
// Calculate consistent sizes for layout
|
|
let textHeight = measureText("8", font: digitUIFont).height // Use 8 as reference height
|
|
|
|
// 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
|
|
let vSpacing = portrait ? baseFontSize * 0.22 : baseFontSize * 0.30 // More spacing in landscape
|
|
let horizontalSepSize = CGSize(width: dotDiameter * 2 + hSpacing, height: dotDiameter)
|
|
let verticalSepSize = CGSize(width: dotDiameter, height: dotDiameter * 2 + vSpacing)
|
|
|
|
// Calculate layout - simplified without AM/PM
|
|
let (totalWidth, totalHeight) = calculateLayoutSize(
|
|
portrait: portrait,
|
|
horizontalSepSize: horizontalSepSize,
|
|
verticalSepSize: verticalSepSize,
|
|
showSeconds: showSeconds
|
|
)
|
|
|
|
// Calculate scale with maximum space utilization using full screen
|
|
let safeInset = AppConstants.Defaults.safeInset
|
|
let availableW = max(1, fullScreenSize.width - safeInset * 2)
|
|
let availableH = max(1, fullScreenSize.height - safeInset * 2)
|
|
|
|
// Calculate scaling factors
|
|
let widthScale = availableW / max(totalWidth, 1)
|
|
let heightScale = availableH / max(totalHeight, 1)
|
|
|
|
// For stretched mode, use reasonable scaling
|
|
let effectiveScale = stretched ?
|
|
max(0.1, min(min(widthScale, heightScale), 1.5)) : // Use min scale and cap at 1.5x to prevent overflow
|
|
max(0.1, max(0.1, min(widthScale, heightScale)) * CGFloat(max(0.1, min(manualScale, 1.0))))
|
|
|
|
let finalScale = effectiveScale
|
|
|
|
// Time display with consistent centering and stable layout
|
|
Group {
|
|
if portrait {
|
|
VStack(alignment: .center, spacing: 0) {
|
|
TimeSegment(text: hour, fontSize: baseFontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign)
|
|
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 {
|
|
HStack(alignment: .center, spacing: baseFontSize * 0.035) {
|
|
TimeSegment(text: hour, fontSize: baseFontSize, opacity: clockOpacity, digitColor: digitColor, glowIntensity: glowIntensity, fontFamily: fontFamily, fontWeight: fontWeight, fontDesign: fontDesign)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
|
.scaleEffect(finalScale, anchor: .center)
|
|
.animation(UIConstants.AnimationCurves.smooth, value: finalScale)
|
|
.animation(UIConstants.AnimationCurves.smooth, value: showSeconds) // Smooth animation for seconds toggle
|
|
.minimumScaleFactor(0.1)
|
|
.clipped() // Prevent overflow beyond bounds
|
|
}
|
|
.border(.blue, width: 1)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.onOrientationChange() // Force updates on orientation changes
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
private func measureText(_ text: String, font: UIFont) -> CGSize {
|
|
return FontUtils.measureTextSize(text: text, font: font)
|
|
}
|
|
|
|
private func calculateLayoutSize(
|
|
portrait: Bool,
|
|
horizontalSepSize: CGSize,
|
|
verticalSepSize: CGSize,
|
|
showSeconds: Bool
|
|
) -> (CGFloat, CGFloat) {
|
|
// Simplified layout calculation without AM/PM
|
|
// This is just a placeholder since we're using natural sizing now
|
|
if portrait {
|
|
let totalH = horizontalSepSize.height * (showSeconds ? 2 : 1)
|
|
return (0, totalH) // Width will be determined by content
|
|
} else {
|
|
let totalW = verticalSepSize.width * (showSeconds ? 2 : 1)
|
|
return (totalW, 0) // Height will be determined by content
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 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)
|
|
}
|