Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
1a6c1a3c9d
commit
4e02cb1336
94
PRD.md
94
PRD.md
@ -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")
|
- **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
|
- **Perfect centering**: Each digit is centered within its fixed-width container
|
||||||
|
|
||||||
### Font Customization System
|
### Advanced Font Customization System
|
||||||
- **Font family selection**: System, Helvetica, Arial, Times New Roman, Georgia, Verdana, Monaco, Courier options
|
- **Type-safe font selection**: FontFamily enum with System, Helvetica, Arial, Times New Roman, Georgia, Verdana, Courier, Futura, Avenir, Roboto options
|
||||||
- **Weight variations**: 9 weight options from Ultra Light to Black
|
- **Weight variations**: Font.Weight enum with 9 weight options from Ultra Light to Black
|
||||||
- **Design choices**: Default, Serif, Rounded, Monospaced designs
|
- **Design choices**: Font.Design enum with Default, Serif, Rounded, Monospaced designs
|
||||||
- **Live preview**: Real-time font preview in settings interface
|
- **Live preview**: Real-time font preview in settings interface with allCases picker integration
|
||||||
- **UIFont integration**: Proper font measurement for accurate sizing
|
- **Binary search font sizing**: Advanced calculateOptimalFontSize with tight bounding box calculations
|
||||||
- **Weight-based dot scaling**: Colon dots automatically scale with font weight
|
- **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
|
### Dynamic Layout and Sizing
|
||||||
- **GeometryReader integration**: Real-time container size detection
|
- **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
|
- **Responsive updates**: Immediate recalculation on orientation or layout changes
|
||||||
|
|
||||||
### Advanced Spacing and Alignment
|
### 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
|
- **Orientation-aware spacing**: Different spacing values for portrait vs landscape
|
||||||
- **Consistent segment spacing**: Uniform spacing between hours, minutes, seconds
|
- **Consistent segment spacing**: Uniform spacing between hours, minutes, seconds
|
||||||
- **Dot weight matching**: Colon dots scale with selected font weight
|
- **Dot weight matching**: Colon dots scale with selected font weight
|
||||||
- **Overflow prevention**: Spacing calculations prevent content clipping
|
- **Overflow prevention**: Spacing calculations prevent content clipping
|
||||||
- **Perfect centering**: All elements centered both horizontally and vertically
|
- **Perfect centering**: All elements centered both horizontally and vertically
|
||||||
|
- **Component consolidation**: Eliminated redundant HorizontalColon and VerticalColon views
|
||||||
|
|
||||||
### Full-Screen Mode Enhancements
|
### Full-Screen Mode Enhancements
|
||||||
- **Status bar hiding**: Automatic status bar hiding in full-screen mode
|
- **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)
|
- Visual settings (colors, glow, scale, opacity)
|
||||||
- Overlay settings (battery, date, opacity)
|
- Overlay settings (battery, date, opacity)
|
||||||
- Background settings
|
- Background settings
|
||||||
- Font customization (family, weight, design)
|
- Type-safe font customization (FontFamily, Font.Weight, Font.Design enums)
|
||||||
- Color caching for performance optimization
|
- 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
|
- **Alarm**: Codable struct for comprehensive alarm data
|
||||||
- UUID identifier
|
- UUID identifier
|
||||||
- Time and enabled state
|
- Time and enabled state
|
||||||
@ -327,11 +334,13 @@ These principles are fundamental to the project's long-term success and must be
|
|||||||
|
|
||||||
### Settings Interface
|
### Settings Interface
|
||||||
- **Form-based layout**: Organized sections for different setting categories
|
- **Form-based layout**: Organized sections for different setting categories
|
||||||
- **Interactive controls**: Toggles, sliders, color pickers
|
- **Interactive controls**: Toggles, sliders, color pickers, enum-based pickers
|
||||||
- **Real-time updates**: Changes apply immediately
|
- **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
|
- **Sheet presentation**: Modal settings with detents
|
||||||
- **iPad optimization**: Settings sheet opens at full size (.large) on iPad for better usability
|
- **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
|
- **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
|
## File Structure and Organization
|
||||||
|
|
||||||
@ -381,7 +390,11 @@ TheNoiseClock/
|
|||||||
│ │ │ ├── ClockView.swift # Main clock display view
|
│ │ │ ├── ClockView.swift # Main clock display view
|
||||||
│ │ │ ├── ClockSettingsView.swift # Clock customization interface
|
│ │ │ ├── ClockSettingsView.swift # Clock customization interface
|
||||||
│ │ │ └── Components/
|
│ │ │ └── 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
|
│ │ │ ├── BatteryOverlayView.swift # Battery level overlay
|
||||||
│ │ │ ├── DateOverlayView.swift # Date display overlay
|
│ │ │ ├── DateOverlayView.swift # Date display overlay
|
||||||
│ │ │ └── TopOverlayView.swift # Combined overlay container
|
│ │ │ └── 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
|
- **UIKit**: UIFont integration for precise text measurement and font customization
|
||||||
- **UIApplication**: Screen wake lock management and idle timer control
|
- **UIApplication**: Screen wake lock management and idle timer control
|
||||||
|
|
||||||
### Font and Typography Utilities
|
### Advanced Font and Typography Utilities
|
||||||
- **FontUtils.optimalFontSize()**: Calculates optimal font size for portrait orientation
|
- **FontFamily enum**: Type-safe font family selection with allCases support
|
||||||
- **FontUtils.maximumStretchedFontSize()**: Calculates maximum font size for stretched mode
|
- **Font.Weight extension**: Enhanced weight enum with allCases and uiFontWeight properties
|
||||||
- **FontUtils.customFont()**: Creates SwiftUI Font with custom family, weight, and design
|
- **Font.Design extension**: Enhanced design enum with allCases and uiFontWidth properties
|
||||||
- **FontUtils.customUIFont()**: Creates UIFont for precise text measurement
|
- **FontUtils.calculateOptimalFontSize()**: Binary search algorithm for precise font sizing
|
||||||
- **FontUtils.weightMultiplier()**: Calculates dot size multiplier based on font weight
|
- **FontUtils.tightBoundingBox()**: Accurate text measurement with minimal padding
|
||||||
- **FontUtils.calculateMaxTextWidth()**: Measures text width for fixed-width calculations
|
- **FontUtils.createUIFont()**: Creates UIFont instances with proper weight and design mapping
|
||||||
- **FontUtils.calculateMaxTextHeight()**: Measures text height for consistent digit sizing
|
- **FontUtils.weightedFontName()**: Constructs proper font names for custom fonts
|
||||||
- **FontUtils.timePickerFontSize()**: Optimized font sizing for DatePicker components
|
- **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
|
### Performance Considerations
|
||||||
- **Smart timer management**: Conditional timers based on settings
|
- **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
|
- **Dictionary lookups**: O(1) alarm access instead of linear search
|
||||||
- **Smooth animations**: Hardware-accelerated transitions
|
- **Smooth animations**: Hardware-accelerated transitions
|
||||||
- **Preloaded audio**: Instant sound playback
|
- **Preloaded audio**: Instant sound playback
|
||||||
- **Font measurement caching**: Efficient text size calculations
|
- **Binary search font sizing**: O(log n) font size calculation for optimal performance
|
||||||
- **Fixed-width calculations**: Pre-calculated digit dimensions for consistent layout
|
- **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
|
- **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
|
## Future Enhancement Opportunities
|
||||||
- **Additional sound types**: More white noise variants
|
- **Additional sound types**: More white noise variants
|
||||||
- **Sleep timer**: Auto-stop noise after specified time
|
- **Sleep timer**: Auto-stop noise after specified time
|
||||||
|
|||||||
43
TheNoiseClock/Core/Utilities/Font.Design.swift
Normal file
43
TheNoiseClock/Core/Utilities/Font.Design.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
TheNoiseClock/Core/Utilities/Font.Weight.swift
Normal file
83
TheNoiseClock/Core/Utilities/Font.Weight.swift
Normal 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 ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
TheNoiseClock/Core/Utilities/FontFamily.swift
Normal file
36
TheNoiseClock/Core/Utilities/FontFamily.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,86 +6,30 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Font sizing and typography utilities
|
/// Font sizing and typography utilities
|
||||||
struct FontUtils {
|
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
|
let weightSuffix = weight.uiFontWeightSuffix
|
||||||
|
|
||||||
switch design {
|
switch design {
|
||||||
case .rounded:
|
case .rounded:
|
||||||
if name.lowercased() == "system" { return "System" }
|
if name.lowercased() == "system" { return "System" }
|
||||||
return name + (weightSuffix.isEmpty ? "-Rounded" : weightSuffix + "Rounded")
|
return name
|
||||||
|
+ (weightSuffix.isEmpty ? "-Rounded" : weightSuffix + "Rounded")
|
||||||
case .monospaced:
|
case .monospaced:
|
||||||
if name.lowercased() == "system" { return "Courier" }
|
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:
|
case .serif:
|
||||||
if name.lowercased() == "system" { return "TimesNewRomanPS" }
|
if name.lowercased() == "system" { return "TimesNewRomanPS" }
|
||||||
return name + weightSuffix
|
return name + weightSuffix
|
||||||
@ -93,198 +37,104 @@ struct FontUtils {
|
|||||||
return name + weightSuffix
|
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 low: CGFloat = 1.0
|
||||||
var high: CGFloat = 2000.0
|
var high: CGFloat = 2000.0
|
||||||
|
|
||||||
while high - low > 0.01 {
|
while high - low > 0.01 {
|
||||||
let mid = (low + high) / 2
|
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)
|
let textSize = tightBoundingBox(for: digit, withFont: testFont)
|
||||||
|
|
||||||
if textSize.width <= size.width && textSize.height <= size.height {
|
if textSize.width <= size.width && textSize.height <= size.height {
|
||||||
low = mid
|
low = mid
|
||||||
} else {
|
} else {
|
||||||
high = mid
|
high = mid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return low
|
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(
|
let attributedString = NSAttributedString(
|
||||||
string: text,
|
string: text,
|
||||||
attributes: [.font: font]
|
attributes: [.font: font]
|
||||||
)
|
)
|
||||||
let rect = attributedString.boundingRect(
|
let rect = attributedString.boundingRect(
|
||||||
with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
|
with: CGSize(
|
||||||
|
width: CGFloat.greatestFiniteMagnitude,
|
||||||
|
height: CGFloat.greatestFiniteMagnitude
|
||||||
|
),
|
||||||
options: [.usesLineFragmentOrigin, .usesFontLeading],
|
options: [.usesLineFragmentOrigin, .usesFontLeading],
|
||||||
context: nil
|
context: nil
|
||||||
)
|
)
|
||||||
return CGSize(width: ceil(rect.width), height: ceil(rect.height))
|
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 {
|
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 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
|
static func createFont(
|
||||||
/// - Parameter baseFontSize: Base font size
|
name: FontFamily,
|
||||||
/// - 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,
|
|
||||||
weight: Font.Weight,
|
weight: Font.Weight,
|
||||||
design: Font.Design
|
design: Font.Design,
|
||||||
|
size: CGFloat
|
||||||
) -> Font {
|
) -> 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)
|
return .system(size: size, weight: weight, design: design)
|
||||||
} else {
|
} else {
|
||||||
return .custom(family.rawValue, size: size)
|
return .custom(fontName, size: size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,15 +114,18 @@ class ClockStyle: Codable, Equatable {
|
|||||||
self.nightModeEndTime = try container.decodeIfPresent(String.self, forKey: .nightModeEndTime) ?? self.nightModeEndTime
|
self.nightModeEndTime = try container.decodeIfPresent(String.self, forKey: .nightModeEndTime) ?? self.nightModeEndTime
|
||||||
self.autoBrightness = try container.decodeIfPresent(Bool.self, forKey: .autoBrightness) ?? self.autoBrightness
|
self.autoBrightness = try container.decodeIfPresent(Bool.self, forKey: .autoBrightness) ?? self.autoBrightness
|
||||||
self.ambientLightThreshold = try container.decodeIfPresent(Double.self, forKey: .ambientLightThreshold) ?? self.ambientLightThreshold
|
self.ambientLightThreshold = try container.decodeIfPresent(Double.self, forKey: .ambientLightThreshold) ?? self.ambientLightThreshold
|
||||||
// Decode font settings with fallback to string conversion
|
// Decode font settings explicitly from strings to avoid ambiguity
|
||||||
if let fontFamilyString = try container.decodeIfPresent(String.self, forKey: .fontFamily) {
|
if let fontFamilyRaw = try container.decodeIfPresent(String.self, forKey: .fontFamily),
|
||||||
self.fontFamily = FontUtils.fontNameFromString(fontFamilyString)
|
let decoded = FontFamily(rawValue: fontFamilyRaw) {
|
||||||
|
self.fontFamily = decoded
|
||||||
}
|
}
|
||||||
if let fontWeightString = try container.decodeIfPresent(String.self, forKey: .fontWeight) {
|
if let fontWeightString = try container.decodeIfPresent(String.self, forKey: .fontWeight),
|
||||||
self.fontWeight = FontUtils.fontWeightFromString(fontWeightString)
|
let decoded = Font.Weight(rawValue: fontWeightString) {
|
||||||
|
self.fontWeight = decoded
|
||||||
}
|
}
|
||||||
if let fontDesignString = try container.decodeIfPresent(String.self, forKey: .fontDesign) {
|
if let fontDesignString = try container.decodeIfPresent(String.self, forKey: .fontDesign),
|
||||||
self.fontDesign = FontUtils.fontDesignFromString(fontDesignString)
|
let decoded = Font.Design(rawValue: fontDesignString) {
|
||||||
|
self.fontDesign = decoded
|
||||||
}
|
}
|
||||||
self.showBattery = try container.decodeIfPresent(Bool.self, forKey: .showBattery) ?? self.showBattery
|
self.showBattery = try container.decodeIfPresent(Bool.self, forKey: .showBattery) ?? self.showBattery
|
||||||
self.showDate = try container.decodeIfPresent(Bool.self, forKey: .showDate) ?? self.showDate
|
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(autoBrightness, forKey: .autoBrightness)
|
||||||
try container.encode(ambientLightThreshold, forKey: .ambientLightThreshold)
|
try container.encode(ambientLightThreshold, forKey: .ambientLightThreshold)
|
||||||
try container.encode(fontFamily.rawValue, forKey: .fontFamily)
|
try container.encode(fontFamily.rawValue, forKey: .fontFamily)
|
||||||
try container.encode(FontUtils.stringFromFontWeight(fontWeight), forKey: .fontWeight)
|
try container.encode(fontWeight.rawValue, forKey: .fontWeight)
|
||||||
try container.encode(FontUtils.stringFromFontDesign(fontDesign), forKey: .fontDesign)
|
try container.encode(fontDesign.rawValue, forKey: .fontDesign)
|
||||||
try container.encode(showBattery, forKey: .showBattery)
|
try container.encode(showBattery, forKey: .showBattery)
|
||||||
try container.encode(showDate, forKey: .showDate)
|
try container.encode(showDate, forKey: .showDate)
|
||||||
try container.encode(dateFormat, forKey: .dateFormat)
|
try container.encode(dateFormat, forKey: .dateFormat)
|
||||||
|
|||||||
@ -12,16 +12,9 @@ struct TimePickerSection: View {
|
|||||||
@Binding var selectedTime: Date
|
@Binding var selectedTime: Date
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { proxy in
|
GeometryReader { proxy in
|
||||||
let size = proxy.size
|
|
||||||
let portrait = size.height >= size.width
|
|
||||||
|
|
||||||
// Calculate optimal font size for time picker
|
// Calculate optimal font size for time picker
|
||||||
let pickerFontSize = FontUtils.timePickerFontSize(
|
let pickerFontSize = 20.0
|
||||||
containerWidth: size.width,
|
|
||||||
containerHeight: size.height,
|
|
||||||
isPortrait: portrait
|
|
||||||
)
|
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DatePicker(
|
DatePicker(
|
||||||
|
|||||||
@ -248,7 +248,7 @@ private struct FontSection: View {
|
|||||||
// Font Weight
|
// Font Weight
|
||||||
Picker("Weight", selection: $style.fontWeight) {
|
Picker("Weight", selection: $style.fontWeight) {
|
||||||
ForEach(Font.Weight.allCases, id: \.self) { weight in
|
ForEach(Font.Weight.allCases, id: \.self) { weight in
|
||||||
Text(FontUtils.stringFromFontWeight(weight)).tag(weight)
|
Text(weight.rawValue).tag(weight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.menu)
|
.pickerStyle(.menu)
|
||||||
@ -256,7 +256,7 @@ private struct FontSection: View {
|
|||||||
// Font Design
|
// Font Design
|
||||||
Picker("Design", selection: $style.fontDesign) {
|
Picker("Design", selection: $style.fontDesign) {
|
||||||
ForEach(Font.Design.allCases, id: \.self) { design in
|
ForEach(Font.Design.allCases, id: \.self) { design in
|
||||||
Text(FontUtils.stringFromFontDesign(design)).tag(design)
|
Text(design.rawValue).tag(design)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.menu)
|
.pickerStyle(.menu)
|
||||||
@ -267,12 +267,7 @@ private struct FontSection: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("12:34")
|
Text("12:34")
|
||||||
.font(FontUtils.customFont(
|
.font(FontUtils.createFont(name: style.fontFamily, weight: style.fontWeight, design: style.fontDesign, size: 24))
|
||||||
size: 24,
|
|
||||||
family: style.fontFamily,
|
|
||||||
weight: style.fontWeight,
|
|
||||||
design: style.fontDesign
|
|
||||||
))
|
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,7 +73,10 @@ struct DigitView: View {
|
|||||||
private var text: some View {
|
private var text: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
Text(digit)
|
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)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.minimumScaleFactor(0.1)
|
.minimumScaleFactor(0.1)
|
||||||
|
|||||||
@ -17,7 +17,7 @@ struct DotCircle: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// Calculate size based on font weight - make dots smaller for lighter weights
|
// 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
|
let adjustedSize = size * sizeMultiplier
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user