From e4453a62e03338d6a751867a2acd8d9916b07e9c Mon Sep 17 00:00:00 2001 From: swift-man Date: Sat, 27 Jun 2026 14:51:00 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=200.7.2=20custom=20font=20glyph=20blee?= =?UTF-8?q?d=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SignPainter, BlowBrush, Brush Script MT 같은 script/custom font에서 digit glyph가 column 경계 밖으로 그려질 때 잘릴 수 있는 문제를 선택적으로 완화합니다. - AnimateNumberText initializer에 glyphBleed 옵션을 추가했습니다. - 기본값은 EdgeInsets()라 일반 폰트의 기존 폭과 간격은 유지됩니다. - smooth/reel digit column 모두 GlyphBleedClipShape를 통해 clipping rect만 확장합니다. - 음수 glyphBleed 값은 0으로 보정합니다. - read-only initializer의 glyphBleed 컴파일 테스트를 추가했습니다. - README와 DocC 설치 버전 및 custom font clipping 예제를 0.7.2 기준으로 갱신했습니다. 검증: - swift test - DOCS_OUTPUT_PATH=.build/docc-site DOCC_ARCHIVE_PATH=.build/AnimateNumberText.doccarchive ./GeneratingDocumentationSite - git diff --check --- README.md | 20 ++++++++- .../AnimateNumberText.md | 20 ++++++++- .../Public/AnimateNumberText.swift | 43 ++++++++++++++++--- .../AnimateNumberTextTests.swift | 12 ++++++ 4 files changed, 86 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1eb7a14..9849535 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ SwiftUI-Version ![Badge](https://img.shields.io/badge/swift-white.svg?style=flat-square&logo=Swift) ![Badge](https://img.shields.io/badge/SwiftUI-001b87.svg?style=flat-square&logo=Swift&logoColor=black) -![Badge - Version](https://img.shields.io/badge/Version-0.7.1-1177AA?style=flat-square) +![Badge - Version](https://img.shields.io/badge/Version-0.7.2-1177AA?style=flat-square) ![Badge - Swift Package Manager](https://img.shields.io/badge/SPM-compatible-orange?style=flat-square) ![Badge - Platform](https://img.shields.io/badge/platform-mac_12|ios_15|watchos_8|tvos_15-yellow?style=flat-square) ![Badge - License](https://img.shields.io/badge/license-MIT-black?style=flat-square) @@ -158,6 +158,22 @@ For values updated at high frequency, such as drag gestures or timers, throttle or debounce the bound value at the call site when every intermediate value does not need to be rendered. +## Custom Font Clipping + +Some script or brush fonts draw glyphs outside their measured digit column. Use +`glyphBleed` to give each digit column extra clipping space without changing the +default layout for regular fonts. + +```swift +AnimateNumberText(value: $value, + textColor: $textColor, + font: .custom("SignPainter", size: 48), + glyphBleed: EdgeInsets(top: 2, + leading: 4, + bottom: 2, + trailing: 4)) +``` + ## Documentation - [DocC Documentation](https://docs.gorani.me/AnimateNumberText/documentation/animatenumbertext/) @@ -171,6 +187,6 @@ Once you have your Swift package set up, adding AnimateNumberText as a dependenc ```swift dependencies: [ - .package(url: "https://github.com/swift-man/AnimateNumberText.git", from: "0.7.1") + .package(url: "https://github.com/swift-man/AnimateNumberText.git", from: "0.7.2") ] ``` diff --git a/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md b/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md index 74991f6..d13b3ab 100644 --- a/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md +++ b/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md @@ -14,7 +14,7 @@ Add AnimateNumberText to your Swift package dependencies. ```swift dependencies: [ - .package(url: "https://github.com/swift-man/AnimateNumberText.git", from: "0.7.1") + .package(url: "https://github.com/swift-man/AnimateNumberText.git", from: "0.7.2") ] ``` @@ -169,6 +169,24 @@ For values updated at high frequency, such as drag gestures or timers, throttle or debounce the bound value at the call site when every intermediate value does not need to be rendered. +## Custom Font Clipping + +Some script or brush fonts draw glyphs outside their measured digit column. Use +`glyphBleed` to give each digit column extra clipping space without changing the +default layout for regular fonts. + +```swift +AnimateNumberText( + value: $value, + textColor: $textColor, + font: .custom("SignPainter", size: 48), + glyphBleed: EdgeInsets(top: 2, + leading: 4, + bottom: 2, + trailing: 4) +) +``` + ## Topics ### Views diff --git a/Sources/AnimateNumberText/Public/AnimateNumberText.swift b/Sources/AnimateNumberText/Public/AnimateNumberText.swift index a7e7246..348892d 100644 --- a/Sources/AnimateNumberText/Public/AnimateNumberText.swift +++ b/Sources/AnimateNumberText/Public/AnimateNumberText.swift @@ -24,6 +24,7 @@ public struct AnimateNumberText: View { // MARK: - Animation Properties private let animation: AnimateNumberTextAnimation + private let glyphBleed: EdgeInsets @State private var animationRange: [TextColumn] = [] @@ -46,6 +47,7 @@ public struct AnimateNumberText: View { /// - numberFormatter: An optional formatter for numeric presentation. /// - stringFormatter: An optional string format, such as `"%@ ms"`. /// - animation: The animation configuration used for digit updates. + /// - glyphBleed: Extra clipping space for custom fonts whose glyphs draw outside their column. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) public init( font: Font = .largeTitle, @@ -54,13 +56,15 @@ public struct AnimateNumberText: View { textColor: Binding, numberFormatter: NumberFormatter? = nil, stringFormatter: String? = nil, - animation: AnimateNumberTextAnimation = .smooth() + animation: AnimateNumberTextAnimation = .smooth(), + glyphBleed: EdgeInsets = EdgeInsets() ) { self.font = font self.weight = weight self._value = value self._textColor = textColor self.animation = animation + self.glyphBleed = glyphBleed.sanitizedGlyphBleed self.formatter = AnimateNumberTextFormatter(numberFormatter: numberFormatter, stringFormatter: stringFormatter) } @@ -78,6 +82,7 @@ public struct AnimateNumberText: View { /// - numberFormatter: An optional formatter for numeric presentation. /// - stringFormatter: An optional string format, such as `"%@ ms"`. /// - animation: The animation configuration used for digit updates. + /// - glyphBleed: Extra clipping space for custom fonts whose glyphs draw outside their column. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) public init( font: Font = .largeTitle, @@ -86,7 +91,8 @@ public struct AnimateNumberText: View { textColor: Color = .primary, numberFormatter: NumberFormatter? = nil, stringFormatter: String? = nil, - animation: AnimateNumberTextAnimation = .smooth() + animation: AnimateNumberTextAnimation = .smooth(), + glyphBleed: EdgeInsets = EdgeInsets() ) { self.init(font: font, weight: weight, @@ -94,7 +100,8 @@ public struct AnimateNumberText: View { textColor: .constant(textColor), numberFormatter: numberFormatter, stringFormatter: stringFormatter, - animation: animation) + animation: animation, + glyphBleed: glyphBleed) } public var body: some View { @@ -185,7 +192,8 @@ public struct AnimateNumberText: View { ReelDigitColumn(position: position, font: font, weight: weight, - textColor: textColor) + textColor: textColor, + glyphBleed: glyphBleed) .animation(animation.digitAnimation(at: ordinal, digitCount: digitCount), value: position) } else { @@ -215,7 +223,7 @@ public struct AnimateNumberText: View { } .offset(y: settingOffset(for: textType, height: size.height)) } - .clipped() + .clipShape(GlyphBleedClipShape(glyphBleed: glyphBleed)) } } @@ -342,6 +350,7 @@ private struct ReelDigitColumn: View, @preconcurrency Animatable { let font: Font let weight: Font.Weight let textColor: Color + let glyphBleed: EdgeInsets var animatableData: Double { get { position } @@ -372,7 +381,7 @@ private struct ReelDigitColumn: View, @preconcurrency Animatable { } .offset(y: -(CGFloat(fraction) + 1) * size.height) } - .clipped() + .clipShape(GlyphBleedClipShape(glyphBleed: glyphBleed)) } } @@ -388,3 +397,25 @@ private struct ReelDigitColumn: View, @preconcurrency Animatable { return remainder >= 0 ? remainder : remainder + 10 } } + +private extension EdgeInsets { + var sanitizedGlyphBleed: EdgeInsets { + EdgeInsets(top: Swift.max(0, top), + leading: Swift.max(0, leading), + bottom: Swift.max(0, bottom), + trailing: Swift.max(0, trailing)) + } +} + +private struct GlyphBleedClipShape: Shape { + let glyphBleed: EdgeInsets + + func path(in rect: CGRect) -> Path { + let expandedRect = CGRect(x: rect.minX - glyphBleed.leading, + y: rect.minY - glyphBleed.top, + width: rect.width + glyphBleed.leading + glyphBleed.trailing, + height: rect.height + glyphBleed.top + glyphBleed.bottom) + + return Path(expandedRect) + } +} diff --git a/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift b/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift index d9f2729..89616ba 100644 --- a/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift +++ b/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift @@ -182,6 +182,18 @@ struct AnimateNumberTextTests { #expect(String(describing: type(of: view)) == "AnimateNumberText") } + @Test + @MainActor + func readOnlyValueInitializerAcceptsGlyphBleed() { + let view = AnimateNumberText(value: 10.23, + glyphBleed: EdgeInsets(top: 2, + leading: 4, + bottom: 2, + trailing: 4)) + + #expect(String(describing: type(of: view)) == "AnimateNumberText") + } + @Test func resizeForAnimationUsesStablePlaceholders() { var columns = [ From 430b519a2026816654b4fc834ee915e81ca78275 Mon Sep 17 00:00:00 2001 From: swift-man Date: Sat, 27 Jun 2026 15:07:18 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20glyphBleed=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=A7=80=EC=A0=81=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - glyphBleed 음수 입력 보정 테스트를 추가해 clipping 영역이 줄어들지 않도록 회귀를 방지합니다. - GlyphBleedClipShape의 확장 rect 계산 테스트를 추가해 custom font glyph bleed 동작을 직접 검증합니다. - DocC 생성 스크립트의 fallback bundle version을 0.7.2로 갱신합니다. 검증: - swift test - DOCS_OUTPUT_PATH=.build/docc-site DOCC_ARCHIVE_PATH=.build/AnimateNumberText.doccarchive ./GeneratingDocumentationSite - git diff --check --- GeneratingDocumentationSite | 2 +- .../Public/AnimateNumberText.swift | 4 +-- .../AnimateNumberTextTests.swift | 27 +++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/GeneratingDocumentationSite b/GeneratingDocumentationSite index fd03976..d14952c 100755 --- a/GeneratingDocumentationSite +++ b/GeneratingDocumentationSite @@ -9,7 +9,7 @@ DOCC_ARCHIVE_PATH="${DOCC_ARCHIVE_PATH:-./$TARGET_NAME.doccarchive}" DOCS_OUTPUT_PATH="${DOCS_OUTPUT_PATH:-./docs}" HOSTING_BASE_PATH="${HOSTING_BASE_PATH:-$TARGET_NAME}" BUNDLE_IDENTIFIER="${BUNDLE_IDENTIFIER:-com.swift-man.$TARGET_NAME}" -BUNDLE_VERSION="${BUNDLE_VERSION:-0.6.0}" +BUNDLE_VERSION="${BUNDLE_VERSION:-0.7.2}" MINIMUM_ACCESS_LEVEL="${MINIMUM_ACCESS_LEVEL:-public}" DOCC="$(xcrun --find docc)" diff --git a/Sources/AnimateNumberText/Public/AnimateNumberText.swift b/Sources/AnimateNumberText/Public/AnimateNumberText.swift index 348892d..56c7878 100644 --- a/Sources/AnimateNumberText/Public/AnimateNumberText.swift +++ b/Sources/AnimateNumberText/Public/AnimateNumberText.swift @@ -398,7 +398,7 @@ private struct ReelDigitColumn: View, @preconcurrency Animatable { } } -private extension EdgeInsets { +extension EdgeInsets { var sanitizedGlyphBleed: EdgeInsets { EdgeInsets(top: Swift.max(0, top), leading: Swift.max(0, leading), @@ -407,7 +407,7 @@ private extension EdgeInsets { } } -private struct GlyphBleedClipShape: Shape { +struct GlyphBleedClipShape: Shape { let glyphBleed: EdgeInsets func path(in rect: CGRect) -> Path { diff --git a/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift b/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift index 89616ba..4a0ad60 100644 --- a/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift +++ b/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift @@ -194,6 +194,33 @@ struct AnimateNumberTextTests { #expect(String(describing: type(of: view)) == "AnimateNumberText") } + @Test + func glyphBleedSanitizesNegativeInsets() { + let glyphBleed = EdgeInsets(top: -2, + leading: 4, + bottom: -6, + trailing: 8).sanitizedGlyphBleed + + #expect(glyphBleed.top == 0) + #expect(glyphBleed.leading == 4) + #expect(glyphBleed.bottom == 0) + #expect(glyphBleed.trailing == 8) + } + + @Test + func glyphBleedClipShapeExpandsClipRect() { + let shape = GlyphBleedClipShape(glyphBleed: EdgeInsets(top: 2, + leading: 4, + bottom: 6, + trailing: 8)) + let rect = CGRect(x: 10, y: 20, width: 30, height: 40) + + #expect(shape.path(in: rect).boundingRect == CGRect(x: 6, + y: 18, + width: 42, + height: 48)) + } + @Test func resizeForAnimationUsesStablePlaceholders() { var columns = [ From 3abbeda1946f7718ed8a11cb89f7d37a2d0ba3ae Mon Sep 17 00:00:00 2001 From: swift-man Date: Sat, 27 Jun 2026 15:08:26 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20glyphBleed=20=EA=B8=B0=EB=B3=B8=20cl?= =?UTF-8?q?ipping=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - glyphBleed가 zero일 때 GlyphBleedClipShape가 원본 rect를 유지하는지 검증합니다. - .clipShape 전환 이후 기본 동작 동등성을 테스트로 고정합니다. 검증: - swift test - git diff --check --- Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift b/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift index 4a0ad60..7b7e045 100644 --- a/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift +++ b/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift @@ -221,6 +221,14 @@ struct AnimateNumberTextTests { height: 48)) } + @Test + func glyphBleedClipShapeKeepsOriginalRectWithZeroBleed() { + let shape = GlyphBleedClipShape(glyphBleed: EdgeInsets()) + let rect = CGRect(x: 10, y: 20, width: 30, height: 40) + + #expect(shape.path(in: rect).boundingRect == rect) + } + @Test func resizeForAnimationUsesStablePlaceholders() { var columns = [ From 7af6c25d3e5106724977f1f6994963883d8bb874 Mon Sep 17 00:00:00 2001 From: swift-man Date: Sat, 27 Jun 2026 15:19:26 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20glyphBleed=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=98=88=EC=A0=9C=20=EC=9D=B8=EC=9E=90=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README의 custom font clipping 예제에서 font 인자를 value 앞에 배치해 실제 initializer 선언 순서와 맞춥니다. - DocC의 동일 예제도 같은 순서로 수정해 복사해도 컴파일되도록 정리합니다. 검증: - swift test - DOCS_OUTPUT_PATH=.build/docc-site DOCC_ARCHIVE_PATH=.build/AnimateNumberText.doccarchive ./GeneratingDocumentationSite - git diff --check --- README.md | 4 ++-- .../AnimateNumberText.docc/AnimateNumberText.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9849535..bf30efc 100644 --- a/README.md +++ b/README.md @@ -165,9 +165,9 @@ Some script or brush fonts draw glyphs outside their measured digit column. Use default layout for regular fonts. ```swift -AnimateNumberText(value: $value, +AnimateNumberText(font: .custom("SignPainter", size: 48), + value: $value, textColor: $textColor, - font: .custom("SignPainter", size: 48), glyphBleed: EdgeInsets(top: 2, leading: 4, bottom: 2, diff --git a/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md b/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md index d13b3ab..7781000 100644 --- a/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md +++ b/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md @@ -177,9 +177,9 @@ default layout for regular fonts. ```swift AnimateNumberText( + font: .custom("SignPainter", size: 48), value: $value, textColor: $textColor, - font: .custom("SignPainter", size: 48), glyphBleed: EdgeInsets(top: 2, leading: 4, bottom: 2,