From 3464cbeb17fa67fa148e5b84ad6696a08c62b164 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 11 Sep 2025 15:53:43 -0500 Subject: [PATCH] Signed-off-by: Matt Bruce --- TheNoiseClock/Core/Utilities/FontFamily.swift | 57 +++++ TheNoiseClock/Core/Utilities/FontUtils.swift | 194 ++++++++++++++++-- TheNoiseClock/ViewModels/ClockViewModel.swift | 37 +++- .../Views/Clock/ClockSettingsView.swift | 37 +++- .../Components/ClockDisplayContainer.swift | 4 +- .../Views/Clock/Components/DigitView.swift | 27 ++- .../Clock/Components/TimeDisplayView.swift | 4 +- .../Views/Clock/Components/TimeSegment.swift | 3 +- 8 files changed, 330 insertions(+), 33 deletions(-) diff --git a/TheNoiseClock/Core/Utilities/FontFamily.swift b/TheNoiseClock/Core/Utilities/FontFamily.swift index efe4d5b..34d84c1 100644 --- a/TheNoiseClock/Core/Utilities/FontFamily.swift +++ b/TheNoiseClock/Core/Utilities/FontFamily.swift @@ -5,6 +5,7 @@ // Created by Matt Bruce on 9/11/25. // import Foundation +import SwiftUI enum FontFamily: String, CaseIterable { case system = "System" @@ -33,4 +34,60 @@ enum FontFamily: String, CaseIterable { default: self = .system } } + + var fontWeights: [Font.Weight] { + guard let weightDict = Self.fontMapWeights[self.rawValue] else { + return [] + } + return Array(weightDict.keys) + } + + func fontName(weight: Font.Weight) -> String? { + guard let weightDict = Self.fontMapWeights[self.rawValue], + let fontName = weightDict[weight] else { + return nil + } + return fontName + } + + static internal var fontMapWeights: [String: [Font.Weight: String]] = [ + FontFamily.helvetica.rawValue: [ + .regular: "Helvetica", + .light: "Helvetica-Light", + .bold: "Helvetica-Bold" + ], + FontFamily.arial.rawValue: [ + .regular: "ArialMT", + .bold: "Arial-BoldMT" + ], + FontFamily.timesNewRoman.rawValue: [ + .regular: "TimesNewRomanPSMT", + .bold: "TimesNewRomanPS-BoldMT" + ], + FontFamily.georgia.rawValue: [ + .regular: "Georgia", + .bold: "Georgia-Bold" + ], + FontFamily.verdana.rawValue: [ + .regular: "Verdana", + .bold: "Verdana-Bold" + ], + FontFamily.courier.rawValue: [ + .regular: "Courier", + .bold: "Courier-Bold" + ], + FontFamily.futura.rawValue: [ + .regular: "Futura-Medium", + .bold: "Futura-Bold" + ], + FontFamily.avenir.rawValue: [ + .regular: "Avenir-Roman", + .bold: "Avenir-Bold" + ], + FontFamily.roboto.rawValue: [ + .regular: "Roboto-Regular", + .bold: "Roboto-Bold" + ] + ] + } diff --git a/TheNoiseClock/Core/Utilities/FontUtils.swift b/TheNoiseClock/Core/Utilities/FontUtils.swift index c97aab8..51f727c 100644 --- a/TheNoiseClock/Core/Utilities/FontUtils.swift +++ b/TheNoiseClock/Core/Utilities/FontUtils.swift @@ -52,7 +52,7 @@ struct FontUtils { return .system(size: size, weight: weight, design: design) } else { return .custom(weightedFontName( - name: name.rawValue, + name: name, weight: weight, design: design ), size: size) @@ -75,7 +75,7 @@ struct FontUtils { if let font = UIFont( name: weightedFontName( - name: name.rawValue, + name: name, weight: weight, design: design ), @@ -111,29 +111,193 @@ struct FontUtils { } private static func weightedFontName( - name: String, + name: FontFamily, weight: Font.Weight, design: Font.Design ) -> String { - let weightSuffix = weight.uiFontWeightSuffix + // Use exact name from map if available + if let exactName = name.fontName(weight: weight) { + return exactName + } + + // Fallback design handling + var baseName = name.rawValue switch design { case .rounded: - if name.lowercased() == "system" { return "System" } - return name - + (weightSuffix.isEmpty ? "-Rounded" : weightSuffix + "Rounded") + baseName = "ArialRoundedMTBold" // System rounded fallback case .monospaced: - if name.lowercased() == "system" { return "Courier" } - return name == "Courier" - ? name + weightSuffix - : name - + (weightSuffix.isEmpty ? "-Mono" : weightSuffix + "Mono") + baseName = "Courier" case .serif: - if name.lowercased() == "system" { return "TimesNewRomanPS" } - return name + weightSuffix + baseName = "TimesNewRomanPSMT" default: - return name + weightSuffix + break } + + let weightSuffix = weight.uiFontWeightSuffix + return baseName + (weightSuffix.isEmpty ? "" : "-" + weightSuffix) + } +} + +private struct TestContentView: View { + @State private var digit: String = "138" + @State private var previewFontSize: CGFloat = 1000 + @State private var fontWeight: Font.Weight = .bold + @State private var fontDesign: Font.Design = .rounded + + private var date: Date { + let hours = ["13"].randomElement() ?? "08" + let minutes = ["38"].randomElement() ?? "33" + let timeString = "\(hours):\(minutes)" + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + let todayString = formatter.string(from: Date()) + let fullDateString = "\(todayString) \(timeString)" + + formatter.dateFormat = "yyyy-MM-dd HH:mm" + return formatter.date(from: fullDateString) ?? Date() } + var body: some View { + let newDate = date + return ScrollView { + VStack(spacing: 20) { + ForEach(FontFamily.allCases, id: \.rawValue) { (font: FontFamily) in + VStack { + Text(font.rawValue) + .font(Font.system(size: 16, weight: Font.Weight.bold)) + .padding(.bottom, 5) + + TimeDisplayView(date: newDate, + use24Hour: true, + showSeconds: false, + digitColor: .primary, + glowIntensity: 0.5, + manualScale: 1.0, + stretched: false, + clockOpacity: 1.0, + fontFamily: font, + fontWeight: fontWeight, + fontDesign: fontDesign, + forceHorizontalMode: true) + + TimeSegment( + text: digit, + fontSize: .constant(129), // CGFloat + opacity: 1.0, // Double + digitColor: Color.primary, // Color + glowIntensity: 0.5, // Double + fontFamily: font, // FontFamily + fontWeight: fontWeight, + fontDesign: fontDesign, + ) + } + .frame(width: 400, height: 200) + .border(Color.black) + + } + } + .padding() + } + } } + +struct FontCombinationsPreview: View { + // All designs + private let designs: [Font.Design] = Font.Design.allCases + + private let columns: [GridItem] = [ + GridItem(.flexible(), spacing: 20), + GridItem(.flexible(), spacing: 20), + GridItem(.flexible(), spacing: 20) + ] + + var body: some View { + // Precompute local copies to avoid type-checker stress + let fonts: [FontFamily] = FontFamily.allCases.sorted { + $0.rawValue.localizedCaseInsensitiveCompare($1.rawValue) + == .orderedAscending + } + let designsLocal: [Font.Design] = designs + + return ScrollView { + LazyVGrid(columns: columns, spacing: 20) { + ForEach(fonts, id: \.self) { font in + ForEach(font.fontWeights, id: \.self) { weight in + ForEach(designsLocal, id: \.self) { design in + FontSampleCell(font: font, weight: weight, design: design) + } + } + } + } + .padding() + } + } +} + +private struct FontSampleCell: View { + let font: FontFamily + let weight: Font.Weight + let design: Font.Design + + var body: some View { + VStack(alignment: .center, spacing: 5) { + Text("\(font.rawValue), \(weight.rawValue), \(design.rawValue)") + .font(.system(size: 12, weight: .medium)) + .multilineTextAlignment(.center) + .padding(.bottom, 5) + TimeSegment( + text: "38", + fontSize: .constant(129), // CGFloat + opacity: 1.0, // Double + digitColor: Color.primary, // Color + glowIntensity: 0.5, // Double + fontFamily: font, // FontFamily + fontWeight: weight, + fontDesign: design) + .frame(width: 100, height: 100) + .border(Color.black) + } + } +} + + +#Preview { + FontCombinationsPreview() +} + + +// MARK: - Descriptions for Font.Weight and Font.Design +private extension Font.Weight { + var description: String { + 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: + // Fallback for any future/unknown cases + return "custom" + } + } +} + +private extension Font.Design { + var description: String { + switch self { + case .default: return "default" + case .serif: return "serif" + case .rounded: return "rounded" + case .monospaced: return "monospaced" + @unknown default: + return "unknown" + } + } +} + diff --git a/TheNoiseClock/ViewModels/ClockViewModel.swift b/TheNoiseClock/ViewModels/ClockViewModel.swift index fd19346..ac0feb3 100644 --- a/TheNoiseClock/ViewModels/ClockViewModel.swift +++ b/TheNoiseClock/ViewModels/ClockViewModel.swift @@ -69,7 +69,42 @@ class ClockViewModel { } func updateStyle(_ newStyle: ClockStyle) { - style = newStyle + DebugLogger.log("ClockViewModel: updateStyle called", category: .settings) + DebugLogger.log("ClockViewModel: old fontFamily = \(style.fontFamily)", category: .settings) + DebugLogger.log("ClockViewModel: new fontFamily = \(newStyle.fontFamily)", category: .settings) + + // Update properties of the existing style object instead of replacing it + // This preserves the @Observable chain + style.use24Hour = newStyle.use24Hour + style.showSeconds = newStyle.showSeconds + style.forceHorizontalMode = newStyle.forceHorizontalMode + style.digitColorHex = newStyle.digitColorHex + style.glowIntensity = newStyle.glowIntensity + style.digitScale = newStyle.digitScale + style.stretched = newStyle.stretched + style.clockOpacity = newStyle.clockOpacity + style.fontFamily = newStyle.fontFamily + style.fontWeight = newStyle.fontWeight + style.fontDesign = newStyle.fontDesign + style.showBattery = newStyle.showBattery + style.showDate = newStyle.showDate + style.overlayOpacity = newStyle.overlayOpacity + style.backgroundHex = newStyle.backgroundHex + style.keepAwake = newStyle.keepAwake + style.randomizeColor = newStyle.randomizeColor + style.selectedColorTheme = newStyle.selectedColorTheme + style.nightModeEnabled = newStyle.nightModeEnabled + style.autoNightMode = newStyle.autoNightMode + style.scheduledNightMode = newStyle.scheduledNightMode + style.nightModeStartTime = newStyle.nightModeStartTime + style.nightModeEndTime = newStyle.nightModeEndTime + style.ambientLightThreshold = newStyle.ambientLightThreshold + style.autoBrightness = newStyle.autoBrightness + style.dateFormat = newStyle.dateFormat + style.respectFocusModes = newStyle.respectFocusModes + + DebugLogger.log("ClockViewModel: after update fontFamily = \(style.fontFamily)", category: .settings) + saveStyle() updateTimersIfNeeded() updateWakeLockState() diff --git a/TheNoiseClock/Views/Clock/ClockSettingsView.swift b/TheNoiseClock/Views/Clock/ClockSettingsView.swift index 37c0089..8ffbaf1 100644 --- a/TheNoiseClock/Views/Clock/ClockSettingsView.swift +++ b/TheNoiseClock/Views/Clock/ClockSettingsView.swift @@ -235,6 +235,15 @@ private struct FontSection: View { // Use the enum allCases for font options + // Computed property for available weights based on selected font + private var availableWeights: [Font.Weight] { + if style.fontFamily == .system { + return Font.Weight.allCases + } else { + return style.fontFamily.fontWeights + } + } + var body: some View { Section(header: Text("Font")) { // Font Family @@ -244,22 +253,36 @@ private struct FontSection: View { } } .pickerStyle(.menu) + .onChange(of: style.fontFamily) { _, newFamily in + // Auto-set design to default for non-system fonts + if newFamily != .system { + style.fontDesign = .default + } + + // Auto-set weight to first available weight if current weight is not available + let availableWeights = newFamily == .system ? Font.Weight.allCases : newFamily.fontWeights + if !availableWeights.contains(style.fontWeight) { + style.fontWeight = availableWeights.first ?? .regular + } + } - // Font Weight + // Font Weight - show available weights for selected font Picker("Weight", selection: $style.fontWeight) { - ForEach(Font.Weight.allCases, id: \.self) { weight in + ForEach(availableWeights, id: \.self) { weight in Text(weight.rawValue).tag(weight) } } .pickerStyle(.menu) - // Font Design - Picker("Design", selection: $style.fontDesign) { - ForEach(Font.Design.allCases, id: \.self) { design in - Text(design.rawValue).tag(design) + // Font Design - only show for system font + if style.fontFamily == .system { + Picker("Design", selection: $style.fontDesign) { + ForEach(Font.Design.allCases, id: \.self) { design in + Text(design.rawValue).tag(design) + } } + .pickerStyle(.menu) } - .pickerStyle(.menu) // Font Preview HStack { diff --git a/TheNoiseClock/Views/Clock/Components/ClockDisplayContainer.swift b/TheNoiseClock/Views/Clock/Components/ClockDisplayContainer.swift index 039c091..9c82de7 100644 --- a/TheNoiseClock/Views/Clock/Components/ClockDisplayContainer.swift +++ b/TheNoiseClock/Views/Clock/Components/ClockDisplayContainer.swift @@ -17,7 +17,9 @@ struct ClockDisplayContainer: View { // MARK: - Body var body: some View { - GeometryReader { geometry in + DebugLogger.log("ClockDisplayContainer: body called with fontFamily=\(style.fontFamily), fontWeight=\(style.fontWeight), fontDesign=\(style.fontDesign)", category: .general) + + return GeometryReader { geometry in let isPortrait = geometry.size.height >= geometry.size.width let hasOverlay = style.showBattery || style.showDate let topSpacing = hasOverlay ? (isPortrait ? UIConstants.Spacing.huge : UIConstants.Spacing.large) : 0 diff --git a/TheNoiseClock/Views/Clock/Components/DigitView.swift b/TheNoiseClock/Views/Clock/Components/DigitView.swift index 4104319..62afa5c 100644 --- a/TheNoiseClock/Views/Clock/Components/DigitView.swift +++ b/TheNoiseClock/Views/Clock/Components/DigitView.swift @@ -11,13 +11,13 @@ import SwiftUI struct DigitView: View { @Environment(\.sizeCategory) private var sizeCategory - @State var digit: String - @State var fontName: FontFamily - @State var weight: Font.Weight - @State var design: Font.Design - @State var opacity: Double - @State var digitColor: Color - @State var glowIntensity: Double + let digit: String + let fontName: FontFamily + let weight: Font.Weight + let design: Font.Design + let opacity: Double + let digitColor: Color + let glowIntensity: Double @Binding var fontSize: CGFloat @State private var lastCalculatedSize: CGSize = .zero @@ -31,6 +31,7 @@ struct DigitView: View { opacity: Double = 1, glowIntensity: Double = 0, fontSize: Binding) { + DebugLogger.log("DigitView: init called with fontName=\(fontName), weight=\(weight), design=\(design)", category: .general) self.digit = (digit.count == 1 && "0123456789".contains(digit)) ? digit : "0" self.fontName = fontName self.weight = weight @@ -96,6 +97,18 @@ struct DigitView: View { .onChange(of: sizeCategory) { _, _ in calculateOptimalFontSize(for: geometry.size) } + .onChange(of: fontName) { _, _ in + DebugLogger.log("DigitView: fontName changed to \(fontName)", category: .general) + calculateOptimalFontSize(for: geometry.size) + } + .onChange(of: weight) { _, _ in + DebugLogger.log("DigitView: weight changed to \(weight)", category: .general) + calculateOptimalFontSize(for: geometry.size) + } + .onChange(of: design) { _, _ in + DebugLogger.log("DigitView: design changed to \(design)", category: .general) + calculateOptimalFontSize(for: geometry.size) + } } } diff --git a/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift b/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift index e3a5a8c..6a18bb0 100644 --- a/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift +++ b/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift @@ -56,7 +56,9 @@ struct TimeDisplayView: View { // MARK: - Body var body: some View { - GeometryReader { proxy in + DebugLogger.log("TimeDisplayView: body called with fontFamily=\(fontFamily), fontWeight=\(fontWeight), fontDesign=\(fontDesign)", category: .general) + + return GeometryReader { proxy in let containerSize = proxy.size let portraitMode = containerSize.height >= containerSize.width let portrait = !forceHorizontalMode && containerSize.height >= containerSize.width diff --git a/TheNoiseClock/Views/Clock/Components/TimeSegment.swift b/TheNoiseClock/Views/Clock/Components/TimeSegment.swift index e05a5fa..dc14677 100644 --- a/TheNoiseClock/Views/Clock/Components/TimeSegment.swift +++ b/TheNoiseClock/Views/Clock/Components/TimeSegment.swift @@ -20,7 +20,8 @@ struct TimeSegment: View { let fontDesign: Font.Design var body: some View { - HStack(alignment: .center, spacing: 0) { + DebugLogger.log("TimeSegment: body called with fontFamily=\(fontFamily), fontWeight=\(fontWeight), fontDesign=\(fontDesign)", category: .general) + return HStack(alignment: .center, spacing: 0) { ForEach(Array(text.enumerated()), id: \.offset) { index, character in DigitView( digit: String(character),