Add `badge` circle style (#7)

Fixes #3


Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
This commit is contained in:
Joni Van Roost 2019-01-25 22:37:06 +01:00 committed by Sindre Sorhus
parent cc5918267a
commit c4766cb481
5 changed files with 142 additions and 6 deletions

View File

@ -12,14 +12,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
borrowIconFrom(app: "Photos")
var lastStyleWasBar = true
let styles: [DockProgress.ProgressStyle] = [
.bar,
.circle(radius: 58, color: .systemPink),
.badge(color: .systemBlue, badgeValue: { Int(DockProgress.progressValue * 12) })
]
var stylesIterator = styles.makeIterator()
let _ = stylesIterator.next()
Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { _ in
DockProgress.progressValue += 0.01
if DockProgress.progressValue > 1 {
DockProgress.progressValue = 0
DockProgress.style = lastStyleWasBar ? .circle(radius: 58, color: .systemPink) : .bar
lastStyleWasBar = !lastStyleWasBar
if let style = stylesIterator.next() {
DockProgress.progressValue = 0
DockProgress.style = style
} else {
// Reset iterator when all is looped
stylesIterator = styles.makeIterator()
}
}
}
}

View File

@ -39,6 +39,7 @@ public final class DockProgress {
case bar
/// TODO: Make `color` optional when https://github.com/apple/swift-evolution/blob/master/proposals/0155-normalize-enum-case-representation.md is shipping in Swift
case circle(radius: Double, color: NSColor)
case badge(color: NSColor, badgeValue: () -> Int)
case custom(drawHandler: (_ rect: CGRect) -> Void)
}
@ -65,6 +66,8 @@ public final class DockProgress {
self.drawProgressBar(dstRect)
case let .circle(radius, color):
self.drawProgressCircle(dstRect, radius: radius, color: color)
case let .badge(color, badgeValue):
self.drawProgressBadge(dstRect, color: color, badgeLabel: badgeValue())
case let .custom(drawingHandler):
drawingHandler(dstRect)
}
@ -104,4 +107,84 @@ public final class DockProgress {
progressCircle.progress = progressValue
progressCircle.render(in: cgContext)
}
private static func drawProgressBadge(_ dstRect: CGRect, color: NSColor, badgeLabel: Int) {
guard let cgContext = NSGraphicsContext.current?.cgContext else {
return
}
let radius = dstRect.width / 4.8
let newCenter = CGPoint(x: dstRect.maxX - radius - 4, y: dstRect.minY + radius + 4)
// Background
let badge = ProgressCircleShapeLayer(radius: Double(radius), center: newCenter)
badge.fillColor = CGColor(red: 0.94, green: 0.96, blue: 1, alpha: 1)
badge.shadowColor = .black
badge.shadowOpacity = 0.3
badge.masksToBounds = false
badge.shadowOffset = CGSize(width: -1, height: 1)
badge.shadowPath = badge.path
// Progress circle
let lineWidth: CGFloat = 6
let innerRadius = radius - lineWidth / 2
let progressCircle = ProgressCircleShapeLayer(radius: Double(innerRadius), center: newCenter)
progressCircle.strokeColor = color.cgColor
progressCircle.lineWidth = lineWidth
progressCircle.lineCap = .butt
progressCircle.progress = progressValue
// Label
let dimension = badge.bounds.height - 5
let rect = CGRect(origin: progressCircle.bounds.origin, size: CGSize(width: dimension, height: dimension))
let textLayer = VerticallyCenteredTextLayer(frame: rect, center: newCenter)
let badgeText = kiloShortStringFromInt(number: badgeLabel)
textLayer.foregroundColor = CGColor(red: 0.23, green: 0.23, blue: 0.24, alpha: 1)
textLayer.string = badgeText
textLayer.fontSize = scaledBadgeFontSize(text: badgeText)
textLayer.font = NSFont.helveticaNeueBold
textLayer.alignmentMode = .center
textLayer.truncationMode = .end
badge.addSublayer(textLayer)
badge.addSublayer(progressCircle)
badge.render(in: cgContext)
}
/**
```
999 => 999
1000 => 1K
1100 => 1K
2000 => 2K
10000 => 9K+
```
*/
private static func kiloShortStringFromInt(number: Int) -> String {
let sign = number.signum()
let absNumber = abs(number)
if absNumber < 1000 {
return "\(number)"
} else if absNumber < 10000 {
return "\(sign * Int(absNumber / 1000))k"
} else {
return "\(sign * 9)k+"
}
}
private static func scaledBadgeFontSize(text: String) -> CGFloat {
switch text.count {
case 1:
return 30
case 2:
return 23
case 3:
return 19
case 4:
return 15
default:
return 0
}
}
}

View File

@ -41,8 +41,12 @@ final class ProgressCircleShapeLayer: CAShapeLayer {
self.init()
fillColor = nil
lineCap = .round
path = NSBezierPath.progressCircle(radius: radius, center: center).cgPath
position = center
strokeEnd = 0
let cgPath = NSBezierPath.progressCircle(radius: radius, center: center).cgPath
path = cgPath
bounds = cgPath.boundingBox
}
var progress: Double {
@ -55,13 +59,15 @@ final class ProgressCircleShapeLayer: CAShapeLayer {
}
}
extension NSColor {
func with(alpha: Double) -> NSColor {
return withAlphaComponent(CGFloat(alpha))
}
}
extension NSFont {
static let helveticaNeueBold = NSFont(name: "HelveticaNeue-Bold", size: 0)
}
extension CGRect {
var center: CGPoint {
@ -106,3 +112,24 @@ extension NSBezierPath {
self.init(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
}
}
/// Fixes the vertical alignment issue of the `CATextLayer` class.
final class VerticallyCenteredTextLayer: CATextLayer {
convenience init(frame rect: CGRect, center: CGPoint) {
self.init()
frame = rect
frame.center = center
contentsScale = NSScreen.main?.backingScaleFactor ?? 2
}
// From https://stackoverflow.com/a/44055040/6863743
override func draw(in context: CGContext) {
let height = bounds.size.height
let deltaY = ((height - fontSize) / 2 - fontSize / 10) * -1
context.saveGState()
context.translateBy(x: 0, y: deltaY)
super.draw(in: context)
context.restoreGState()
}
}

View File

@ -97,6 +97,20 @@ DockProgress.style = .circle(radius: 55, color: .systemBlue)
Make sure to set a `radius` that matches your app icon.
### Badge
![](screenshot-badge.gif)
```swift
import DockProgress
DockProgress.style = .badge(color: .systemBlue, badgeValue: { getDownloadCount() })
```
Large `badgeValue` numbers will be written in kilo short notation, for example, `1000``1k`.
Note: The `badgeValue` is not meant to be used as a numeric percentage. It's for things like count of downloads, number of files being converted, etc.
## Related

BIN
screenshot-badge.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB