291 lines
9.5 KiB
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)
|
|
}
|
|
}
|
|
}
|