TheNoiseClock/TheNoiseClock/Core/Utilities/FontUtils.swift

291 lines
9.5 KiB
Swift

//
// FontUtils.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
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
}
}
}
/// Font sizing and typography utilities
struct FontUtils {
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")
case .monospaced:
if name.lowercased() == "system" { return "Courier" }
return name == "Courier" ? name + weightSuffix : name + (weightSuffix.isEmpty ? "-Mono" : weightSuffix + "Mono")
case .serif:
if name.lowercased() == "system" { return "TimesNewRomanPS" }
return name + weightSuffix
default:
return name + weightSuffix
}
}
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 textSize = tightBoundingBox(for: digit, withFont: testFont)
if textSize.width <= size.width && textSize.height <= size.height {
low = mid
} else {
high = mid
}
}
return low
}
private static func tightBoundingBox(for text: String, withFont font: UIFont) -> CGSize {
let attributedString = NSAttributedString(
string: text,
attributes: [.font: font]
)
let rect = attributedString.boundingRect(
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 {
if name == .system {
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) {
return font
}
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,
weight: Font.Weight,
design: Font.Design
) -> Font {
if family == .system {
return .system(size: size, weight: weight, design: design)
} else {
return .custom(family.rawValue, size: size)
}
}
}