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

This commit is contained in:
Matt Bruce 2025-09-08 11:53:37 -05:00
parent 2d4d76cc8d
commit d7897d6b38
3 changed files with 141 additions and 53 deletions

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "20B61D3C-CF97-4427-BF7D-EB393A559DFC"
type = "1"
version = "2.0">
</Bucket>

View File

@ -207,6 +207,27 @@ enum FontUtils {
)
}
/// Calculate the maximum width for any two-digit combination
/// - Parameters:
/// - font: The font to measure with
/// - fontSize: The font size
/// - Returns: Maximum width for any two-digit combination
static func maxTwoDigitWidth(font: UIFont, fontSize: CGFloat) -> CGFloat {
let digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
var maxWidth: CGFloat = 0
// Test all possible two-digit combinations to find the widest
for firstDigit in digits {
for secondDigit in digits {
let combination = firstDigit + secondDigit
let width = measureTextSize(text: combination, font: font).width
maxWidth = max(maxWidth, width)
}
}
return maxWidth
}
/// Get weight multiplier for visual consistency with font weight
/// - Parameter weight: Font weight name
/// - Returns: Multiplier for dot size (0.7 to 1.3)

View File

@ -113,14 +113,25 @@ struct TimeDisplayView: View {
design: fontDesign
)
// Use fixed-width calculations to prevent layout jumping
let digitWidth = FontUtils.maxDigitWidth(font: digitUIFont)
// Calculate consistent sizes for layout
let digitHeight = measureText("8", font: digitUIFont).height // Use 8 as reference height
// Fixed sizes for consistent layout
let hourSize = CGSize(width: digitWidth * 2, height: digitHeight) // Two digits
let minuteSize = CGSize(width: digitWidth * 2, height: digitHeight) // Two digits
let secondsSize = showSeconds ? CGSize(width: digitWidth * 2, height: digitHeight) : .zero
// Calculate the width of "88" for consistent sizing
let testFont = FontUtils.customUIFont(
size: baseFontSize,
family: fontFamily,
weight: fontWeight,
design: fontDesign
)
let _ = calculateMaxTextWidth(font: testFont)
// Calculate width for a single digit (using "8" as the widest)
let singleDigitWidth = calculateMaxTextWidth(font: testFont, text: "8")
// All time segments use the same fixed width to prevent shifting
let hourSize = CGSize(width: singleDigitWidth * 2, height: digitHeight)
let minuteSize = CGSize(width: singleDigitWidth * 2, height: digitHeight)
let secondsSize = showSeconds ? CGSize(width: singleDigitWidth * 2, height: digitHeight) : .zero
let ampmSize = showAMPM ? measureText(ampmText, font: ampmUIFont) : .zero
// Separators - reasonable spacing
@ -273,7 +284,15 @@ struct TimeDisplayView: View {
}
// MARK: - Supporting Views
private struct TimeSegment: View {
// Calculate width of text for the given font - this ensures consistent width
private func calculateMaxTextWidth(font: UIFont, text: String = "88") -> CGFloat {
let attributes = [NSAttributedString.Key.font: font]
let size = (text as NSString).size(withAttributes: attributes)
return size.width
}
private struct TimeSegment: View {
let text: String
let fontSize: CGFloat
let opacity: Double
@ -291,10 +310,50 @@ private struct TimeSegment: View {
weight: fontWeight,
design: fontDesign
)
let maxWidth = FontUtils.maxDigitWidth(font: font)
let singleDigitWidth = calculateMaxTextWidth(font: font, text: "8")
let totalWidth = singleDigitWidth * CGFloat(text.count)
HStack(spacing: 0) {
ForEach(Array(text.enumerated()), id: \.offset) { index, character in
DigitView(
digit: String(character),
fontSize: fontSize,
opacity: clamped,
digitColor: digitColor,
glowIntensity: glowIntensity,
fontFamily: fontFamily,
fontWeight: fontWeight,
fontDesign: fontDesign,
digitWidth: singleDigitWidth
)
}
}
.frame(width: totalWidth, alignment: .center)
.border(Color.blue.opacity(0.5), width: 1)
}
// Calculate width of text for the given font - this ensures consistent width
private func calculateMaxTextWidth(font: UIFont, text: String = "88") -> CGFloat {
let attributes = [NSAttributedString.Key.font: font]
let size = (text as NSString).size(withAttributes: attributes)
return size.width
}
}
private struct DigitView: View {
let digit: String
let fontSize: CGFloat
let opacity: Double
let digitColor: Color
let glowIntensity: Double
let fontFamily: String
let fontWeight: String
let fontDesign: String
let digitWidth: CGFloat
var body: some View {
ZStack {
Text(text)
Text(digit)
.font(FontUtils.customFont(
size: fontSize,
family: fontFamily,
@ -303,8 +362,9 @@ private struct TimeSegment: View {
))
.foregroundColor(digitColor)
.blur(radius: ColorUtils.glowRadius(intensity: glowIntensity))
.opacity(ColorUtils.glowOpacity(intensity: glowIntensity) * clamped)
Text(text)
.opacity(ColorUtils.glowOpacity(intensity: glowIntensity) * opacity)
.multilineTextAlignment(.center)
Text(digit)
.font(FontUtils.customFont(
size: fontSize,
family: fontFamily,
@ -312,15 +372,16 @@ private struct TimeSegment: View {
design: fontDesign
))
.foregroundColor(digitColor)
.opacity(clamped)
}
.frame(width: maxWidth * CGFloat(text.count), height: nil, alignment: .center)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(1)
.allowsTightening(false) // Prevent tightening to maintain fixed width
.opacity(opacity)
.multilineTextAlignment(.center)
}
}
.frame(width: digitWidth, alignment: .center)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(1)
.allowsTightening(false)
.multilineTextAlignment(.center)
}
}
private struct HorizontalColon: View {
let dotDiameter: CGFloat