Last active
July 23, 2023 09:29
-
-
Save yechentide/85f0018b0b533d6094d8da3dee2714f9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
要素数が多すぎたり、gapが大きすぎると、うまく表示されないバグがあるので、ご注意ください。
数個のデータを表示するような限定的な場面では使えるかもしれないです。
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
preview