Last active
October 29, 2024 21:15
-
-
Save calleric/4f3f55a249fd66dd08440e62fb165360 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
// gist file for Creating Activity Rings in SwiftUI | |
// https://swdevnotes.com/swift/2021/create-activity-rings-in-swiftui/ | |
// | |
// All code in one ContentView.swift file just for sharing | |
// | |
// Created by Eric on 05/09/2021. | |
// | |
import SwiftUI | |
struct ContentView: View { | |
@State private var progressValues = [0.0, 0.0, 0.0, 0.0] | |
var body: some View { | |
ZStack { | |
Color(red: 208/255, green: 225/255, blue: 242/255, opacity: 0.4) | |
.edgesIgnoringSafeArea(.all) | |
VStack { | |
ZStack() { | |
ActivityRingView(progress: progressValues[0], | |
ringRadius: 140.0, | |
thickness: 20.0, | |
startColor: Color(red: 1.000, green: 0.596, blue: 0.588), | |
endColor: Color(red: 0.839, green: 0.153, blue: 0.157)) | |
ActivityRingView(progress: progressValues[1], | |
ringRadius: 120.0, | |
thickness: 20.0, | |
startColor: Color(red: 0.596, green: 0.875, blue: 0.541), | |
endColor: Color(red: 0.173, green: 0.627, blue: 0.173)) | |
ActivityRingView(progress: progressValues[2], | |
ringRadius: 100.0, | |
thickness: 20.0, | |
startColor: Color(red: 1.000, green: 0.733, blue: 0.471), | |
endColor: Color(red: 1.000, green: 0.498, blue: 0.055)) | |
ActivityRingView(progress: progressValues[3], | |
ringRadius: 80.0, | |
thickness: 20.0, | |
startColor: Color(red: 0.784, green: 0.659, blue: 0.941), | |
endColor: Color(red: 0.278, green: 0.129, blue: 0.620)) | |
} | |
.frame(height:350) | |
Spacer().frame(height:50) | |
HStack(spacing:20) { | |
Spacer() | |
Button("Increase Activity") { | |
progressValues = progressValues.map { ($0 + Double.random(in: 0.2...0.5)) } | |
} | |
Button("Reset") { | |
progressValues = [0.0, 0.0, 0.0, 0.0] | |
} | |
Spacer() | |
} | |
Spacer() | |
} | |
.padding() | |
.animation(.linear(duration:1.0)) | |
} | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView() | |
} | |
} | |
struct ActivityRingView: View { | |
var progress: Double | |
var ringRadius: Double = 60.0 | |
var thickness: CGFloat = 20.0 | |
var startColor = Color(red: 0.784, green: 0.659, blue: 0.941) | |
var endColor = Color(red: 0.278, green: 0.129, blue: 0.620) | |
private var ringTipShadowOffset: CGPoint { | |
let ringTipPosition = tipPosition(progress: progress, radius: ringRadius) | |
let shadowPosition = tipPosition(progress: progress + 0.0075, radius: ringRadius) | |
return CGPoint(x: shadowPosition.x - ringTipPosition.x, | |
y: shadowPosition.y - ringTipPosition.y) | |
} | |
private func tipPosition(progress:Double, radius:Double) -> CGPoint { | |
let progressAngle = Angle(degrees: (360.0 * progress) - 90.0) | |
return CGPoint( | |
x: radius * cos(progressAngle.radians), | |
y: radius * sin(progressAngle.radians)) | |
} | |
var body: some View { | |
let activityAngularGradient = AngularGradient( | |
gradient: Gradient(colors: [startColor, endColor]), | |
center: .center, | |
startAngle: .degrees(0), | |
endAngle: .degrees(360.0 * progress)) | |
ZStack { | |
Circle() | |
.stroke(startColor.opacity(0.15), lineWidth: thickness) | |
.frame(width:CGFloat(ringRadius) * 2.0) | |
Circle() | |
.stroke(Color(.systemGray2), lineWidth: 1.0) | |
.frame(width:(CGFloat(ringRadius) * 2.0) + thickness) | |
Circle() | |
.stroke(Color(.systemGray2), lineWidth: 1.0) | |
.frame(width:(CGFloat(ringRadius) * 2.0) - thickness) | |
Circle() | |
.trim(from: 0, to: CGFloat(self.progress)) | |
.stroke( | |
activityAngularGradient, | |
style: StrokeStyle(lineWidth: thickness, lineCap: .round)) | |
.rotationEffect(Angle(degrees: -90)) | |
.frame(width:CGFloat(ringRadius) * 2.0) | |
ActivityRingTip(progress: progress, | |
ringRadius: Double(ringRadius)) | |
.fill(progress>0.95 ? endColor : .clear) | |
.frame(width:thickness, height:thickness) | |
.shadow(color: progress>0.95 ? .black.opacity(0.3) : .clear, | |
radius: 2.5, | |
x: ringTipShadowOffset.x, | |
y: ringTipShadowOffset.y) | |
} | |
} | |
} | |
struct ActivityRingTip: Shape { | |
var progress: Double | |
var ringRadius: Double | |
private var position: CGPoint { | |
let progressAngle = Angle(degrees: (360.0 * progress) - 90.0) | |
return CGPoint( | |
x: ringRadius * cos(progressAngle.radians), | |
y: ringRadius * sin(progressAngle.radians)) | |
} | |
var animatableData: Double { | |
get { progress } | |
set { progress = newValue } | |
} | |
func path(in rect: CGRect) -> Path { | |
var path = Path() | |
if progress > 0.0 { | |
path.addEllipse(in: CGRect( | |
x: position.x, | |
y: position.y, | |
width: rect.size.width, | |
height: rect.size.height)) | |
} | |
return path | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you!