Skip to content

Instantly share code, notes, and snippets.

@yechentide
Last active July 23, 2023 09:29
Show Gist options
  • Save yechentide/85f0018b0b533d6094d8da3dee2714f9 to your computer and use it in GitHub Desktop.
Save yechentide/85f0018b0b533d6094d8da3dee2714f9 to your computer and use it in GitHub Desktop.
import SwiftUI
// https://gist.github.com/Koshimizu-Takehito/93c5891e89d65eadf2164351ca1f2d76
typealias ChartElement = (name: String, count: Int, color: Color)
struct AnimatedDonutChart: View {
@State private var progress: Double = 0
let data: [ChartElement]
let gapDegree: Double
let lineWidth: CGFloat
let duration: Double
var filteredData: [ChartElement] {
data.filter { $0.count > 0 }
}
var totalCount: Double {
data.reduce(0) { $0 + Double($1.count) }
}
var availableDegrees: Double {
360 - gapDegree * Double(filteredData.count)
}
var arcDegrees: [Double] {
if filteredData.count == 1 {
return [360]
}
filteredData.map {
Double($0.count) / totalCount * availableDegrees
}
}
var offsetDegrees: [Double] {
guard filteredData.count > 1 else {
return [0]
}
var degrees: [Double] = []
for index in 0..<filteredData.count {
if index == 0 {
degrees.append(gapDegree)
continue
}
let degree = degrees[index-1] + arcDegrees[index-1] + gapDegree
degrees.append(degree)
}
return degrees
}
var body: some View {
ZStack {
ForEach(0..<filteredData.count, id: \.self) { index in
DonutArc(
start: min(progress, offsetDegrees[index]),
end: min(progress, offsetDegrees[index] + arcDegrees[index]),
lineWidth: lineWidth
)
.stroke(style: .init(lineWidth: lineWidth, lineCap: .round))
.foregroundColor(filteredData[index].color)
.rotationEffect(Angle(degrees: progress / 2))
}
}
.background(.blue.opacity(0.1))
.onAppear {
animate()
}
}
func animate() {
progress = 0
withAnimation(.spring(response: duration, dampingFraction: 0.9)) {
progress = 360
}
}
}
struct DonutArc: Shape, Animatable {
var start: Double
var end: Double
var lineWidth: CGFloat
var animatableData: AnimatablePair<Double, Double> {
get { AnimatablePair(start, end) }
set { (start, end) = (newValue.first, newValue.second) }
}
func path(in rect: CGRect) -> Path {
let radius: CGFloat = min(rect.width, rect.height)/2 - lineWidth/2
let center = CGPoint(x: rect.midX, y: rect.midY)
let start = Angle(degrees: start)
let end = Angle(degrees: end)
var path = Path()
path.addArc(center: center, radius: radius, startAngle: start, endAngle: end, clockwise: false)
return path
}
}
struct AnimatedDonutChart_Previews: PreviewProvider {
static func randomColor() -> Color {
Color(red: CGFloat.random(in: 0...1),
green: CGFloat.random(in: 0...1),
blue: CGFloat.random(in: 0...1))
}
static let data: [ChartElement] = [
("AAA", 30, randomColor()),
("BBB", 20, randomColor()),
("CCC", 20, randomColor()),
("DDD", 05, randomColor()),
("EEE", 05, randomColor()),
("FFF", 10, randomColor()),
("GGG", 00, randomColor()),
("HHH", 28, randomColor()),
]
static var previews: some View {
ZStack {
AnimatedDonutChart(data: data, gapDegree: 6, lineWidth: 10, duration: 3)
CountText(endCount: data.reduce(0) { $0 + $1.count }, duration: 2.5)
.font(.title)
}
.frame(width: 250, height: 250)
}
}
import SwiftUI
import Combine
struct KeyCountText: View {
let endCount: Int
let duration: Double
@State private var currentCount: Double
let increase: Double
let timer: Publishers.Autoconnect<Timer.TimerPublisher>
var displayedCount: Int {
abs(Int(floor(currentCount)))
}
init(endCount: Int, duration: Double) {
self.endCount = endCount
self.duration = duration
_currentCount = State(initialValue: 0)
self.increase = Double(endCount) / duration / 100
self.timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
}
var body: some View {
Text("\(displayedCount)")
.scaleEffect(currentCount == -Double(endCount) ? 1.5 : 1)
.animation(.spring(response: 0.4, dampingFraction: 0.5), value: currentCount)
.onReceive(timer) { _ in
currentCount += increase
if currentCount < Double(endCount) {
return
}
timer.upstream.connect().cancel()
// fire scale animation
currentCount = -Double(endCount)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
currentCount = Double(endCount)
}
}
}
}
struct KeyCountText_Previews: PreviewProvider {
static var previews: some View {
KeyCountText(endCount: 20356819637, duration: 3)
}
}
@yechentide
Copy link
Author

@yechentide
Copy link
Author

要素数が多すぎたり、gapが大きすぎると、うまく表示されないバグがあるので、ご注意ください。
数個のデータを表示するような限定的な場面では使えるかもしれないです。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment