Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion GeneratingDocumentationSite
Original file line number Diff line number Diff line change
Expand Up @@ -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)"

Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(font: .custom("SignPainter", size: 48),
value: $value,
textColor: $textColor,
glyphBleed: EdgeInsets(top: 2,
Comment thread
swift-man marked this conversation as resolved.
leading: 4,
bottom: 2,
trailing: 4))
```

## Documentation

- [DocC Documentation](https://docs.gorani.me/AnimateNumberText/documentation/animatenumbertext/)
Expand All @@ -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")
]
```
Original file line number Diff line number Diff line change
Expand Up @@ -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")
]
```

Expand Down Expand Up @@ -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(
font: .custom("SignPainter", size: 48),
value: $value,
Comment thread
swift-man marked this conversation as resolved.
Comment thread
swift-man marked this conversation as resolved.
textColor: $textColor,
glyphBleed: EdgeInsets(top: 2,
leading: 4,
bottom: 2,
trailing: 4)
)
```

## Topics

### Views
Expand Down
43 changes: 37 additions & 6 deletions Sources/AnimateNumberText/Public/AnimateNumberText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public struct AnimateNumberText: View {

// MARK: - Animation Properties
private let animation: AnimateNumberTextAnimation
private let glyphBleed: EdgeInsets

@State
private var animationRange: [TextColumn] = []
Expand All @@ -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,
Expand All @@ -54,13 +56,15 @@ public struct AnimateNumberText: View {
textColor: Binding<Color>,
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)
}
Expand All @@ -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,
Expand All @@ -86,15 +91,17 @@ 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,
value: .constant(value),
textColor: .constant(textColor),
numberFormatter: numberFormatter,
stringFormatter: stringFormatter,
animation: animation)
animation: animation,
glyphBleed: glyphBleed)
}

public var body: some View {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -215,7 +223,7 @@ public struct AnimateNumberText: View {
}
.offset(y: settingOffset(for: textType, height: size.height))
}
.clipped()
.clipShape(GlyphBleedClipShape(glyphBleed: glyphBleed))
Comment thread
swift-man marked this conversation as resolved.
Comment thread
swift-man marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -372,7 +381,7 @@ private struct ReelDigitColumn: View, @preconcurrency Animatable {
}
.offset(y: -(CGFloat(fraction) + 1) * size.height)
}
.clipped()
.clipShape(GlyphBleedClipShape(glyphBleed: glyphBleed))
Comment thread
swift-man marked this conversation as resolved.
}
}

Expand All @@ -388,3 +397,25 @@ private struct ReelDigitColumn: View, @preconcurrency Animatable {
return remainder >= 0 ? remainder : remainder + 10
}
}

extension EdgeInsets {
Comment thread
swift-man marked this conversation as resolved.
var sanitizedGlyphBleed: EdgeInsets {
EdgeInsets(top: Swift.max(0, top),
leading: Swift.max(0, leading),
bottom: Swift.max(0, bottom),
trailing: Swift.max(0, trailing))
}
}

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)
}
}
47 changes: 47 additions & 0 deletions Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,53 @@ struct AnimateNumberTextTests {
#expect(String(describing: type(of: view)) == "AnimateNumberText")
}

@Test
@MainActor
func readOnlyValueInitializerAcceptsGlyphBleed() {
Comment thread
swift-man marked this conversation as resolved.
Comment thread
swift-man marked this conversation as resolved.
Comment thread
swift-man marked this conversation as resolved.
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 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 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 = [
Expand Down
Loading