diff --git a/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..1004f88 --- /dev/null +++ b/TheNoiseClock.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/TheNoiseClock/Core/Utilities/FontUtils.swift b/TheNoiseClock/Core/Utilities/FontUtils.swift index a46a7fe..f2c3433 100644 --- a/TheNoiseClock/Core/Utilities/FontUtils.swift +++ b/TheNoiseClock/Core/Utilities/FontUtils.swift @@ -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) diff --git a/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift b/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift index f713d5c..64ac668 100644 --- a/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift +++ b/TheNoiseClock/Views/Clock/Components/TimeDisplayView.swift @@ -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,54 +284,104 @@ struct TimeDisplayView: View { } // MARK: - Supporting Views -private struct TimeSegment: View { - let text: String - let fontSize: CGFloat - let opacity: Double - let digitColor: Color - let glowIntensity: Double - let fontFamily: String - let fontWeight: String - let fontDesign: String - - var body: some View { - let clamped = ColorUtils.clampOpacity(opacity) - let font = FontUtils.customUIFont( - size: fontSize, - family: fontFamily, - weight: fontWeight, - design: fontDesign - ) - let maxWidth = FontUtils.maxDigitWidth(font: font) - - ZStack { - Text(text) - .font(FontUtils.customFont( - size: fontSize, - family: fontFamily, - weight: fontWeight, - design: fontDesign - )) - .foregroundColor(digitColor) - .blur(radius: ColorUtils.glowRadius(intensity: glowIntensity)) - .opacity(ColorUtils.glowOpacity(intensity: glowIntensity) * clamped) - Text(text) - .font(FontUtils.customFont( - size: fontSize, - family: fontFamily, - weight: fontWeight, - 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 - .multilineTextAlignment(.center) + + // 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 + let digitColor: Color + let glowIntensity: Double + let fontFamily: String + let fontWeight: String + let fontDesign: String + + var body: some View { + let clamped = ColorUtils.clampOpacity(opacity) + let font = FontUtils.customUIFont( + size: fontSize, + family: fontFamily, + weight: fontWeight, + design: fontDesign + ) + 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(digit) + .font(FontUtils.customFont( + size: fontSize, + family: fontFamily, + weight: fontWeight, + design: fontDesign + )) + .foregroundColor(digitColor) + .blur(radius: ColorUtils.glowRadius(intensity: glowIntensity)) + .opacity(ColorUtils.glowOpacity(intensity: glowIntensity) * opacity) + .multilineTextAlignment(.center) + Text(digit) + .font(FontUtils.customFont( + size: fontSize, + family: fontFamily, + weight: fontWeight, + design: fontDesign + )) + .foregroundColor(digitColor) + .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