Skip to content

Instantly share code, notes, and snippets.

@leeeboo
Forked from networkextension/ActivityRing2.swift
Created June 24, 2021 03:46
Show Gist options
  • Save leeeboo/9b5bfe53ae1328bdd2bca570452b0b46 to your computer and use it in GitHub Desktop.
Save leeeboo/9b5bfe53ae1328bdd2bca570452b0b46 to your computer and use it in GitHub Desktop.
Final code for part 2 of the article on recreating the Apple Watch activity rings in SwiftUI. add Timer base Animation
/// Nested activity rings
struct ActivityRings: View {
var ringGap: CGFloat = 2
@State var progressMove: Double
@State var progressExercise: Double
@State var progressStand: Double
var body: some View {
ZStack{
GeometryReader { geo in
RadialGradient (gradient: Gradient (colors: [Color.black, Color.black]),
center: .center, startRadius: 5, endRadius: 500)
.scaleEffect(1.2)
ActivityRing( progressMax: progressMove,
lineWidth: geo.size.width/10,
gradient: .activityMove)
ActivityRing(progress: 0.0,progressMax: progressExercise,
lineWidth: geo.size.width/10,
gradient: .activityExercise)
.padding(geo.size.width/10 + ringGap)
ActivityRing(progress: 0.0, progressMax: progressStand,
lineWidth: geo.size.width/10,
gradient: .activityStand)
.padding(2 * (geo.size.width/10 + ringGap))
}
}
VStack {
Slider(value: $progressMove, in: 0...2).accentColor(Gradient.activityMove.stops.first!.color)
Slider(value: $progressExercise, in: 0...2).accentColor(Gradient.activityExercise.stops.first!.color)
Slider(value: $progressStand, in: 0...2).accentColor(Gradient.activityStand.stops.first!.color)
}.padding().frame(width: 200, height: 100, alignment: .center)
}
}
/// Base activity ring
struct ActivityRing: View {
@State var progress: Double = 0.0
var progressMax: Double
var lineWidth: CGFloat
var gradient: Gradient
let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
// Background ring
Circle()
.stroke(
gradient.stops.last!.color,
style: .init(lineWidth: lineWidth)
)
.opacity(0.1)
// Main ring
Circle()
.rotation(.degrees(-90))
.trim(from: 0, to: CGFloat(progress))
.stroke(
angularGradient(),
style: .init(lineWidth: lineWidth, lineCap: .round)
)
.overlay(
GeometryReader { geometry in
// End round butt and shadow
Circle()
.fill(endButtColor())
.frame(width: self.lineWidth, height: self.lineWidth)
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
.offset(x: min(geometry.size.width, geometry.size.height)/2)
.rotationEffect(.degrees(self.progress * 360 - 90))
.shadow(color: .black, radius: self.lineWidth/4, x: 0, y: 0)
}
.clipShape(
// Clip end round line cap and shadow to front
Circle()
.rotation(.degrees(-90 + self.progress * 360 - 0.5))
.trim(from: 0, to: 0.25)
.stroke(style: .init(lineWidth: self.lineWidth))
)
)
}.onReceive(timer) { t in
withAnimation {
if progress >= progressMax {
//progress = 0
} else {
progress += 0.2
}
}
}.onTapGesture {
progress = 0.0
}
.scaledToFit()
.padding(lineWidth/2)
}
func angularGradient() -> AngularGradient {
return AngularGradient(gradient: gradient, center: .center, startAngle: .degrees(-90), endAngle: .degrees((progress > 0.5 ? progress : 0.5) * 360 - 90))
}
func endButtColor() -> Color {
let color = progress > 0.5 ? gradient.stops.last!.color : gradient.stops.first!.color.interpolateTo(color: gradient.stops.last!.color, fraction: 2 * progress)
return color
}
}
/// Activity app gradient colors
extension Gradient {
static var activityMove: Gradient {
return Gradient(colors: [
Color(red: 0.8823529412, green: 0, blue: 0.07843137255),
Color(red: 1, green: 0.1960784314, blue: 0.5294117647)
])
}
static var activityExercise: Gradient {
Gradient(colors: [
Color(red: 0.2156862745, green: 0.862745098, blue: 0),
Color(red: 0.7176470588, green: 1, blue: 0)
])
}
static var activityStand: Gradient {
Gradient(colors: [
Color(red: 0, green: 0.7294117647, blue: 0.8823529412),
Color(red: 0, green: 0.9803921569, blue: 0.8156862745)
])
}
}
/// SwiftUI color interpolation
extension Color {
var components: (r: Double, g: Double, b: Double, o: Double)? {
let uiColor: UIColor
var r: CGFloat = 0
var g: CGFloat = 0
var b: CGFloat = 0
var o: CGFloat = 0
if self.description.contains("NamedColor") {
let lowerBound = self.description.range(of: "name: \"")!.upperBound
let upperBound = self.description.range(of: "\", bundle")!.lowerBound
let assetsName = String(self.description[lowerBound..<upperBound])
uiColor = UIColor(named: assetsName)!
} else {
uiColor = UIColor(self)
}
guard uiColor.getRed(&r, green: &g, blue: &b, alpha: &o) else { return nil }
return (Double(r), Double(g), Double(b), Double(o))
}
func interpolateTo(color: Color, fraction: Double) -> Color {
let s = self.components!
let t = color.components!
let r: Double = s.r + (t.r - s.r) * fraction
let g: Double = s.g + (t.g - s.g) * fraction
let b: Double = s.b + (t.b - s.b) * fraction
let o: Double = s.o + (t.o - s.o) * fraction
return Color(red: r, green: g, blue: b, opacity: o)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ActivityRings(progressMove: 0.8, progressExercise: 0.8, progressStand: 0.8)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment