Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-09-11 13:30:55 -05:00
parent 1a6c1a3c9d
commit 4e02cb1336
10 changed files with 334 additions and 280 deletions

94
PRD.md
View File

@ -112,13 +112,17 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di
- **Layout stability**: No shifting or jumping when time changes (e.g., "11" to "12")
- **Perfect centering**: Each digit is centered within its fixed-width container
### Font Customization System
- **Font family selection**: System, Helvetica, Arial, Times New Roman, Georgia, Verdana, Monaco, Courier options
- **Weight variations**: 9 weight options from Ultra Light to Black
- **Design choices**: Default, Serif, Rounded, Monospaced designs
- **Live preview**: Real-time font preview in settings interface
- **UIFont integration**: Proper font measurement for accurate sizing
- **Weight-based dot scaling**: Colon dots automatically scale with font weight
### Advanced Font Customization System
- **Type-safe font selection**: FontFamily enum with System, Helvetica, Arial, Times New Roman, Georgia, Verdana, Courier, Futura, Avenir, Roboto options
- **Weight variations**: Font.Weight enum with 9 weight options from Ultra Light to Black
- **Design choices**: Font.Design enum with Default, Serif, Rounded, Monospaced designs
- **Live preview**: Real-time font preview in settings interface with allCases picker integration
- **Binary search font sizing**: Advanced calculateOptimalFontSize with tight bounding box calculations
- **Dynamic font sizing**: Real-time font size optimization based on container geometry
- **UIFont integration**: Proper font measurement with createUIFont and tightBoundingBox utilities
- **Weight-based dot scaling**: Colon dots automatically scale with font weight using dotSizeMultiplier
- **Enum-based architecture**: Type-safe font selection eliminates string-based errors
- **Legacy compatibility**: Backward compatibility methods for existing code integration
### Dynamic Layout and Sizing
- **GeometryReader integration**: Real-time container size detection
@ -128,11 +132,13 @@ TheNoiseClock is a SwiftUI-based iOS application that combines a customizable di
- **Responsive updates**: Immediate recalculation on orientation or layout changes
### Advanced Spacing and Alignment
- **Unified colon component**: Single ColonView with isHorizontal parameter for both orientations
- **Orientation-aware spacing**: Different spacing values for portrait vs landscape
- **Consistent segment spacing**: Uniform spacing between hours, minutes, seconds
- **Dot weight matching**: Colon dots scale with selected font weight
- **Overflow prevention**: Spacing calculations prevent content clipping
- **Perfect centering**: All elements centered both horizontally and vertically
- **Component consolidation**: Eliminated redundant HorizontalColon and VerticalColon views
### Full-Screen Mode Enhancements
- **Status bar hiding**: Automatic status bar hiding in full-screen mode
@ -227,9 +233,10 @@ These principles are fundamental to the project's long-term success and must be
- Visual settings (colors, glow, scale, opacity)
- Overlay settings (battery, date, opacity)
- Background settings
- Font customization (family, weight, design)
- Type-safe font customization (FontFamily, Font.Weight, Font.Design enums)
- Color caching for performance optimization
- Persistent storage with JSON encoding/decoding
- Persistent storage with JSON encoding/decoding and enum-to-string conversion
- Backward compatibility for legacy string-based font settings
- **Alarm**: Codable struct for comprehensive alarm data
- UUID identifier
- Time and enabled state
@ -327,11 +334,13 @@ These principles are fundamental to the project's long-term success and must be
### Settings Interface
- **Form-based layout**: Organized sections for different setting categories
- **Interactive controls**: Toggles, sliders, color pickers
- **Real-time updates**: Changes apply immediately
- **Interactive controls**: Toggles, sliders, color pickers, enum-based 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
- **Enum-based architecture**: Type-safe picker selections eliminate string-based errors
## File Structure and Organization
@ -381,7 +390,11 @@ TheNoiseClock/
│ │ │ ├── ClockView.swift # Main clock display view
│ │ │ ├── ClockSettingsView.swift # Clock customization interface
│ │ │ └── Components/
│ │ │ ├── TimeDisplayView.swift # Advanced segmented time display with fixed-width digits
│ │ │ ├── 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
@ -544,15 +557,20 @@ 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
### Font and Typography Utilities
- **FontUtils.optimalFontSize()**: Calculates optimal font size for portrait orientation
- **FontUtils.maximumStretchedFontSize()**: Calculates maximum font size for stretched mode
- **FontUtils.customFont()**: Creates SwiftUI Font with custom family, weight, and design
- **FontUtils.customUIFont()**: Creates UIFont for precise text measurement
- **FontUtils.weightMultiplier()**: Calculates dot size multiplier based on font weight
- **FontUtils.calculateMaxTextWidth()**: Measures text width for fixed-width calculations
- **FontUtils.calculateMaxTextHeight()**: Measures text height for consistent digit sizing
- **FontUtils.timePickerFontSize()**: Optimized font sizing for DatePicker components
### 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
- **Font.Design extension**: Enhanced design enum with allCases and uiFontWidth properties
- **FontUtils.calculateOptimalFontSize()**: Binary search algorithm for precise font sizing
- **FontUtils.tightBoundingBox()**: Accurate text measurement with minimal padding
- **FontUtils.createUIFont()**: Creates UIFont instances with proper weight and design mapping
- **FontUtils.weightedFontName()**: Constructs proper font names for custom fonts
- **FontUtils.stringFromFontWeight()**: Converts Font.Weight enum to display strings
- **FontUtils.stringFromFontDesign()**: Converts Font.Design enum to display strings
- **Legacy compatibility methods**: Backward compatibility for existing code
- FontUtils.optimalFontSize() and maximumStretchedFontSize()
- FontUtils.customFont() and customUIFont()
- FontUtils.dotSizeMultiplier() and timePickerFontSize()
### Performance Considerations
- **Smart timer management**: Conditional timers based on settings
@ -563,10 +581,40 @@ The following changes **automatically require** PRD updates:
- **Dictionary lookups**: O(1) alarm access instead of linear search
- **Smooth animations**: Hardware-accelerated transitions
- **Preloaded audio**: Instant sound playback
- **Font measurement caching**: Efficient text size calculations
- **Fixed-width calculations**: Pre-calculated digit dimensions for consistent layout
- **Binary search font sizing**: O(log n) font size calculation for optimal performance
- **Tight bounding box calculations**: Precise text measurement with minimal overhead
- **Dynamic font sizing**: Real-time font optimization based on container geometry
- **Component consolidation**: Reduced view hierarchy with unified ColonView
- **Type-safe enums**: Compile-time safety eliminates runtime string conversion overhead
- **Orientation-aware sizing**: Optimized font sizing algorithms for different orientations
## Recent Architectural Improvements
### Font System Refactoring (September 2025)
- **Type-Safe Font Architecture**: Migrated from string-based font selection to enum-based system
- FontFamily enum with 10 font options (System, Helvetica, Arial, Times New Roman, Georgia, Verdana, Courier, Futura, Avenir, Roboto)
- Font.Weight extension with allCases support and uiFontWeight mapping
- Font.Design extension with allCases support and uiFontWidth mapping
- **Advanced Font Sizing**: Implemented binary search algorithm for optimal font sizing
- calculateOptimalFontSize() with tight bounding box calculations
- Real-time font size optimization based on container geometry
- Precise text measurement with minimal padding and spacing
- **Component Consolidation**: Unified colon separator components
- Replaced HorizontalColon and VerticalColon with single ColonView
- Added isHorizontal boolean parameter for orientation control
- Reduced code duplication by ~80 lines
- **Backward Compatibility**: Maintained compatibility with existing code
- Legacy methods preserved for existing functionality
- String-to-enum conversion in ClockStyle for UserDefaults persistence
- Gradual migration path for all font-related components
### Code Quality Improvements
- **Enum-Based Architecture**: Eliminated string-based font selection errors
- **Type Safety**: Compile-time safety for font family, weight, and design selection
- **Performance Optimization**: O(log n) font sizing with binary search
- **Maintainability**: Single source of truth for font options with allCases
- **User Experience**: Real-time font preview with immediate visual feedback
## Future Enhancement Opportunities
- **Additional sound types**: More white noise variants
- **Sleep timer**: Auto-stop noise after specified time

View File

@ -0,0 +1,43 @@
//
// Font.Design.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/11/25.
//
import SwiftUI
extension Font.Design: @retroactive RawRepresentable, @retroactive CaseIterable {
public init?(rawValue: String) {
switch rawValue {
case "Default": self = .default
case "Serif": self = .serif
case "Rounded": self = .rounded
case "Monospaced": self = .monospaced
default: self = .rounded
}
}
public var rawValue: String {
switch self {
case .default: return "Default"
case .serif: return "Serif"
case .rounded: return "Rounded"
case .monospaced: return "Monospaced"
@unknown default: return "Default"
}
}
public static var allCases: [Font.Design] {
[.default, .rounded, .monospaced, .serif]
}
var uiFontWidth: UIFont.Width {
switch self {
case .default: return .standard
case .rounded: return .standard
case .monospaced: return .condensed
case .serif: return .standard
@unknown default:
return .standard
}
}
}

View File

@ -0,0 +1,83 @@
//
// Font.Weight.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/11/25.
//
import SwiftUI
extension Font.Weight: @retroactive RawRepresentable, @retroactive CaseIterable {
public init?(rawValue: String) {
switch rawValue.lowercased() {
case "ultra light", "ultralight":
self = .ultraLight
case "thin":
self = .thin
case "light":
self = .light
case "regular":
self = .regular
case "medium":
self = .medium
case "semibold", "semi bold":
self = .semibold
case "bold":
self = .bold
case "heavy":
self = .heavy
case "black":
self = .black
default:
return nil
}
}
public var rawValue: String {
switch self {
case .ultraLight: return "Ultra Light"
case .thin: return "Thin"
case .light: return "Light"
case .regular: return "Regular"
case .medium: return "Medium"
case .semibold: return "Semibold"
case .bold: return "Bold"
case .heavy: return "Heavy"
case .black: return "Black"
default: return "Regular"
}
}
public static var allCases: [Font.Weight] {
[.ultraLight, .thin, .light, .regular, .medium, .semibold, .bold, .heavy, .black]
}
var uiFontWeight: UIFont.Weight {
switch self {
case .ultraLight: return .ultraLight
case .thin: return .thin
case .light: return .light
case .regular: return .regular
case .medium: return .medium
case .semibold: return .semibold
case .bold: return .bold
case .heavy: return .heavy
case .black: return .black
default: return .regular
}
}
var uiFontWeightSuffix: String {
switch self {
case .ultraLight: return "-UltraLight"
case .thin: return "-Thin"
case .light: return "-Light"
case .regular: return ""
case .medium: return "-Medium"
case .semibold: return "-SemiBold"
case .bold: return "-Bold"
case .heavy: return "-Heavy"
case .black: return "-Black"
default: return ""
}
}
}

View File

@ -0,0 +1,36 @@
//
// FontFamily.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/11/25.
//
import Foundation
enum FontFamily: String, CaseIterable {
case system = "System"
case helvetica = "Helvetica"
case arial = "Arial"
case timesNewRoman = "TimesNewRomanPS"
case georgia = "Georgia"
case verdana = "Verdana"
case courier = "Courier"
case futura = "Futura"
case avenir = "Avenir"
case roboto = "Roboto"
init?(rawValue: String) {
switch rawValue {
case "System": self = .system
case "Helvetica": self = .helvetica
case "Arial": self = .arial
case "Times New Roman": self = .timesNewRoman
case "Georgia": self = .georgia
case "Verdana": self = .verdana
case "Monaco", "Courier": self = .courier
case "Futura": self = .futura
case "Avenir": self = .avenir
case "Roboto": self = .roboto
default: self = .system
}
}
}

View File

@ -6,86 +6,30 @@
//
import Foundation
import UIKit
import SwiftUI
enum FontFamily: String, CaseIterable {
case system = "System"
case helvetica = "Helvetica"
case arial = "Arial"
case timesNewRoman = "TimesNewRomanPS"
case georgia = "Georgia"
case verdana = "Verdana"
case courier = "Courier"
case futura = "Futura"
case avenir = "Avenir"
case roboto = "Roboto"
}
extension Font.Weight {
static var allCases: [Font.Weight] {
[.ultraLight, .thin, .light, .regular, .medium, .semibold, .bold, .heavy, .black]
}
var uiFontWeight: UIFont.Weight {
switch self {
case .ultraLight: return .ultraLight
case .thin: return .thin
case .light: return .light
case .regular: return .regular
case .medium: return .medium
case .semibold: return .semibold
case .bold: return .bold
case .heavy: return .heavy
case .black: return .black
default: return .regular
}
}
var uiFontWeightSuffix: String {
switch self {
case .ultraLight: return "-UltraLight"
case .thin: return "-Thin"
case .light: return "-Light"
case .regular: return ""
case .medium: return "-Medium"
case .semibold: return "-SemiBold"
case .bold: return "-Bold"
case .heavy: return "-Heavy"
case .black: return "-Black"
default: return ""
}
}
}
extension Font.Design {
static var allCases: [Font.Design] {
[.default, .rounded, .monospaced, .serif]
}
var uiFontWidth: UIFont.Width {
switch self {
case .default: return .standard
case .rounded: return .standard
case .monospaced: return .condensed
case .serif: return .standard
@unknown default:
return .standard
}
}
}
import UIKit
/// Font sizing and typography utilities
struct FontUtils {
static func weightedFontName(name: String, weight: Font.Weight, design: Font.Design) -> String {
static func weightedFontName(
name: String,
weight: Font.Weight,
design: Font.Design
) -> String {
let weightSuffix = weight.uiFontWeightSuffix
switch design {
case .rounded:
if name.lowercased() == "system" { return "System" }
return name + (weightSuffix.isEmpty ? "-Rounded" : weightSuffix + "Rounded")
return name
+ (weightSuffix.isEmpty ? "-Rounded" : weightSuffix + "Rounded")
case .monospaced:
if name.lowercased() == "system" { return "Courier" }
return name == "Courier" ? name + weightSuffix : name + (weightSuffix.isEmpty ? "-Mono" : weightSuffix + "Mono")
return name == "Courier"
? name + weightSuffix
: name
+ (weightSuffix.isEmpty ? "-Mono" : weightSuffix + "Mono")
case .serif:
if name.lowercased() == "system" { return "TimesNewRomanPS" }
return name + weightSuffix
@ -93,198 +37,104 @@ struct FontUtils {
return name + weightSuffix
}
}
static func calculateOptimalFontSize(digit: String, fontName: FontFamily, weight: Font.Weight, design: Font.Design, for size: CGSize) -> CGFloat {
static func calculateOptimalFontSize(
digit: String,
fontName: FontFamily,
weight: Font.Weight,
design: Font.Design,
for size: CGSize
) -> CGFloat {
var low: CGFloat = 1.0
var high: CGFloat = 2000.0
while high - low > 0.01 {
let mid = (low + high) / 2
let testFont = createUIFont(name: fontName, weight: weight, design: design, size: mid)
let testFont = createUIFont(
name: fontName,
weight: weight,
design: design,
size: mid
)
let textSize = tightBoundingBox(for: digit, withFont: testFont)
if textSize.width <= size.width && textSize.height <= size.height {
low = mid
} else {
high = mid
}
}
return low
}
private static func tightBoundingBox(for text: String, withFont font: UIFont) -> CGSize {
private static func tightBoundingBox(
for text: String,
withFont font: UIFont
) -> CGSize {
let attributedString = NSAttributedString(
string: text,
attributes: [.font: font]
)
let rect = attributedString.boundingRect(
with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
with: CGSize(
width: CGFloat.greatestFiniteMagnitude,
height: CGFloat.greatestFiniteMagnitude
),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil
)
return CGSize(width: ceil(rect.width), height: ceil(rect.height))
}
static func createUIFont(name: FontFamily, weight: Font.Weight, design: Font.Design, size: CGFloat) -> UIFont {
static func createUIFont(
name: FontFamily,
weight: Font.Weight,
design: Font.Design,
size: CGFloat
) -> UIFont {
if name == .system {
return UIFont.systemFont(ofSize: size, weight: weight.uiFontWeight, width: design.uiFontWidth)
return UIFont.systemFont(
ofSize: size,
weight: weight.uiFontWeight,
width: design.uiFontWidth
)
}
if let font = UIFont(name: weightedFontName(name: name.rawValue, weight: weight, design: design), size: size) {
if let font = UIFont(
name: weightedFontName(
name: name.rawValue,
weight: weight,
design: design
),
size: size
) {
return font
}
return UIFont.systemFont(ofSize: size, weight: weight.uiFontWeight, width: design.uiFontWidth)
return UIFont.systemFont(
ofSize: size,
weight: weight.uiFontWeight,
width: design.uiFontWidth
)
}
/// Calculate AM/PM font size based on base font size
/// - Parameter baseFontSize: Base font size
/// - Returns: AM/PM font size (20% of base)
static func ampmFontSize(baseFontSize: CGFloat) -> CGFloat {
return baseFontSize * 0.20
}
// MARK: - String Conversion Methods (for UserDefaults compatibility)
/// Convert font family string to FontFamily enum (for UserDefaults compatibility)
/// - Parameter family: Font family name
/// - Returns: FontFamily enum
static func fontNameFromString(_ family: String) -> FontFamily {
switch family {
case "System": return .system
case "Helvetica": return .helvetica
case "Arial": return .arial
case "Times New Roman": return .timesNewRoman
case "Georgia": return .georgia
case "Verdana": return .verdana
case "Monaco", "Courier": return .courier
case "Futura": return .futura
case "Avenir": return .avenir
case "Roboto": return .roboto
default: return .system
}
}
/// Convert font weight string to Font.Weight (for UserDefaults compatibility)
/// - Parameter weight: Font weight name
/// - Returns: Font.Weight
static func fontWeightFromString(_ weight: String) -> Font.Weight {
switch weight {
case "Ultra Light": return .ultraLight
case "Thin": return .thin
case "Light": return .light
case "Regular": return .regular
case "Medium": return .medium
case "Semibold": return .semibold
case "Bold": return .bold
case "Heavy": return .heavy
case "Black": return .black
default: return .bold
}
}
/// Convert font design string to Font.Design (for UserDefaults compatibility)
/// - Parameter design: Font design name
/// - Returns: Font.Design
static func fontDesignFromString(_ design: String) -> Font.Design {
switch design {
case "Default": return .default
case "Serif": return .serif
case "Rounded": return .rounded
case "Monospaced": return .monospaced
default: return .rounded
}
}
/// Convert Font.Weight to string for storage
/// - Parameter weight: Font.Weight
/// - Returns: String representation
static func stringFromFontWeight(_ weight: Font.Weight) -> String {
switch weight {
case .ultraLight: return "Ultra Light"
case .thin: return "Thin"
case .light: return "Light"
case .regular: return "Regular"
case .medium: return "Medium"
case .semibold: return "Semibold"
case .bold: return "Bold"
case .heavy: return "Heavy"
case .black: return "Black"
default: return "Regular"
}
}
/// Convert Font.Design to string for storage
/// - Parameter design: Font.Design
/// - Returns: String representation
static func stringFromFontDesign(_ design: Font.Design) -> String {
switch design {
case .default: return "Default"
case .serif: return "Serif"
case .rounded: return "Rounded"
case .monospaced: return "Monospaced"
@unknown default: return "Default"
}
}
/// Time picker font size (legacy method)
static func timePickerFontSize(
containerWidth: CGFloat,
containerHeight: CGFloat,
isPortrait: Bool
) -> CGFloat {
return 20
}
/// Dot size multiplier (legacy method)
static func dotSizeMultiplier(for fontWeight: Font.Weight) -> CGFloat {
return 0.2
}
/// Create a custom font with specified parameters (legacy method)
static func customFont(
size: CGFloat,
family: String,
weight: String,
design: String
) -> Font {
let fontFamily = fontNameFromString(family)
let fontWeight = fontWeightFromString(weight)
let fontDesign = fontDesignFromString(design)
if fontFamily == .system {
return .system(size: size, weight: fontWeight, design: fontDesign)
} else {
return .custom(fontFamily.rawValue, size: size)
}
}
/// Create a custom UIFont with specified parameters (legacy method)
static func customUIFont(
size: CGFloat,
family: String,
weight: String,
design: String
) -> UIFont {
let fontFamily = fontNameFromString(family)
let fontWeight = fontWeightFromString(weight)
let fontDesign = fontDesignFromString(design)
return createUIFont(name: fontFamily, weight: fontWeight, design: fontDesign, size: size)
}
/// Create a custom Font with enum parameters
static func customFont(
size: CGFloat,
family: FontFamily,
static func createFont(
name: FontFamily,
weight: Font.Weight,
design: Font.Design
design: Font.Design,
size: CGFloat
) -> Font {
if family == .system {
let fontName = weightedFontName(
name: name.rawValue,
weight: weight,
design: design
)
print(fontName)
if name == .system {
return .system(size: size, weight: weight, design: design)
} else {
return .custom(family.rawValue, size: size)
return .custom(fontName, size: size)
}
}
}

View File

@ -114,15 +114,18 @@ class ClockStyle: Codable, Equatable {
self.nightModeEndTime = try container.decodeIfPresent(String.self, forKey: .nightModeEndTime) ?? self.nightModeEndTime
self.autoBrightness = try container.decodeIfPresent(Bool.self, forKey: .autoBrightness) ?? self.autoBrightness
self.ambientLightThreshold = try container.decodeIfPresent(Double.self, forKey: .ambientLightThreshold) ?? self.ambientLightThreshold
// Decode font settings with fallback to string conversion
if let fontFamilyString = try container.decodeIfPresent(String.self, forKey: .fontFamily) {
self.fontFamily = FontUtils.fontNameFromString(fontFamilyString)
// Decode font settings explicitly from strings to avoid ambiguity
if let fontFamilyRaw = try container.decodeIfPresent(String.self, forKey: .fontFamily),
let decoded = FontFamily(rawValue: fontFamilyRaw) {
self.fontFamily = decoded
}
if let fontWeightString = try container.decodeIfPresent(String.self, forKey: .fontWeight) {
self.fontWeight = FontUtils.fontWeightFromString(fontWeightString)
if let fontWeightString = try container.decodeIfPresent(String.self, forKey: .fontWeight),
let decoded = Font.Weight(rawValue: fontWeightString) {
self.fontWeight = decoded
}
if let fontDesignString = try container.decodeIfPresent(String.self, forKey: .fontDesign) {
self.fontDesign = FontUtils.fontDesignFromString(fontDesignString)
if let fontDesignString = try container.decodeIfPresent(String.self, forKey: .fontDesign),
let decoded = Font.Design(rawValue: fontDesignString) {
self.fontDesign = decoded
}
self.showBattery = try container.decodeIfPresent(Bool.self, forKey: .showBattery) ?? self.showBattery
self.showDate = try container.decodeIfPresent(Bool.self, forKey: .showDate) ?? self.showDate
@ -155,8 +158,8 @@ class ClockStyle: Codable, Equatable {
try container.encode(autoBrightness, forKey: .autoBrightness)
try container.encode(ambientLightThreshold, forKey: .ambientLightThreshold)
try container.encode(fontFamily.rawValue, forKey: .fontFamily)
try container.encode(FontUtils.stringFromFontWeight(fontWeight), forKey: .fontWeight)
try container.encode(FontUtils.stringFromFontDesign(fontDesign), forKey: .fontDesign)
try container.encode(fontWeight.rawValue, forKey: .fontWeight)
try container.encode(fontDesign.rawValue, forKey: .fontDesign)
try container.encode(showBattery, forKey: .showBattery)
try container.encode(showDate, forKey: .showDate)
try container.encode(dateFormat, forKey: .dateFormat)

View File

@ -12,16 +12,9 @@ struct TimePickerSection: View {
@Binding var selectedTime: Date
var body: some View {
GeometryReader { proxy in
let size = proxy.size
let portrait = size.height >= size.width
GeometryReader { proxy in
// Calculate optimal font size for time picker
let pickerFontSize = FontUtils.timePickerFontSize(
containerWidth: size.width,
containerHeight: size.height,
isPortrait: portrait
)
let pickerFontSize = 20.0
VStack(spacing: 0) {
DatePicker(

View File

@ -248,7 +248,7 @@ private struct FontSection: View {
// Font Weight
Picker("Weight", selection: $style.fontWeight) {
ForEach(Font.Weight.allCases, id: \.self) { weight in
Text(FontUtils.stringFromFontWeight(weight)).tag(weight)
Text(weight.rawValue).tag(weight)
}
}
.pickerStyle(.menu)
@ -256,7 +256,7 @@ private struct FontSection: View {
// Font Design
Picker("Design", selection: $style.fontDesign) {
ForEach(Font.Design.allCases, id: \.self) { design in
Text(FontUtils.stringFromFontDesign(design)).tag(design)
Text(design.rawValue).tag(design)
}
}
.pickerStyle(.menu)
@ -267,12 +267,7 @@ private struct FontSection: View {
.foregroundColor(.secondary)
Spacer()
Text("12:34")
.font(FontUtils.customFont(
size: 24,
family: style.fontFamily,
weight: style.fontWeight,
design: style.fontDesign
))
.font(FontUtils.createFont(name: style.fontFamily, weight: style.fontWeight, design: style.fontDesign, size: 24))
.foregroundColor(.primary)
}
}

View File

@ -73,7 +73,10 @@ struct DigitView: View {
private var text: some View {
GeometryReader { geometry in
Text(digit)
.font(.custom(fontName == .system ? "System" : fontName.rawValue, size: fontSize, relativeTo: .body).weight(weight))
.font(FontUtils.createFont(name: fontName,
weight: weight,
design: design,
size: fontSize))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.multilineTextAlignment(.center)
.minimumScaleFactor(0.1)

View File

@ -17,7 +17,7 @@ struct DotCircle: View {
var body: some View {
// Calculate size based on font weight - make dots smaller for lighter weights
let sizeMultiplier = FontUtils.dotSizeMultiplier(for: fontWeight)
let sizeMultiplier = 0.2
let adjustedSize = size * sizeMultiplier
ZStack {