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")
|
||||
- **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
|
||||
|
||||
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 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
|
||||
@ -94,13 +38,24 @@ struct FontUtils {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -113,178 +68,73 @@ struct FontUtils {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -13,15 +13,8 @@ struct TimePickerSection: View {
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
let size = proxy.size
|
||||
let portrait = size.height >= size.width
|
||||
|
||||
// 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(
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user