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

This commit is contained in:
Matt Bruce 2025-09-11 15:53:43 -05:00
parent 6c5e0b4d74
commit 3464cbeb17
8 changed files with 330 additions and 33 deletions

View File

@ -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"
]
]
}

View File

@ -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"
}
}
}

View File

@ -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()

View File

@ -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
}
// Font Weight
// 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 - 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
// 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)
}
// Font Preview
HStack {

View File

@ -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

View File

@ -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<CGFloat>) {
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)
}
}
}

View File

@ -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

View File

@ -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),