Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save seanlilmateus/7a0dcc6e57ec1296a4b890fe5cc68ac1 to your computer and use it in GitHub Desktop.
Save seanlilmateus/7a0dcc6e57ec1296a4b890fe5cc68ac1 to your computer and use it in GitHub Desktop.
//
// CircularProgressView.swift
// SwiftUI30WWDC2021
//
// Created by Mateo on 5/6/22.
//
// dependency: https://gist.github.com/mattyoung/5f5c0f13c06d980e823481e0334c00fe
import SwiftUI
struct AnimatablePercentModifier: AnimatableModifier {
var animatableData: Double
init(number: Double) {
animatableData = number
}
// break apart the percent formatted string and make the last two digits monospace
// to make the display text stable not jump around as value change
var percentText: Text {
let percentFormatted = animatableData.formatted(.percent.precision(.fractionLength(1)))
let fractionDigit = String(percentFormatted.dropLast().last!)
let decimalSign = String(percentFormatted.dropLast(2).last!)
let lastWholeDigit = String(percentFormatted.dropLast(3).last!)
return Text(percentFormatted.dropLast(4))
+ Text(lastWholeDigit).monospacedDigit() + Text(decimalSign) + Text(fractionDigit).monospacedDigit()
+ Text(String(percentFormatted.last!))
}
func body(content: Content) -> some View {
content
.overlay(percentText)
}
}
extension View {
func animatingPercent(for number: Double) -> some View {
modifier(AnimatablePercentModifier(number: number))
}
}
struct CircularProgressView<RingStyle: ShapeStyle, TextStyle: ShapeStyle>: View {
let progress: Double // 0 to 1 display as percent
let ringStyle: RingStyle
let textStyle: TextStyle
// "SE-0347 Type inference from default expressions" will allow default values:
// init(progress: Double, ringStyle: RingStyle = .tint, textStyle: TextStyle = .primary) {
// as of now, we use conditional extension to provide these defaults
init(progress: Double, ringStyle: RingStyle, textStyle: TextStyle) {
self.progress = progress
self.ringStyle = ringStyle
self.textStyle = textStyle
}
@State private var viewSize = CGSize.zero
var lineWidth: CGFloat {
min(viewSize.width, viewSize.height) * 0.13
}
var body: some View {
Circle()
.trim(from: 0, to: progress)
.stroke(ringStyle, style: .init(lineWidth: lineWidth * 0.7, lineCap: .round))
.rotationEffect(.degrees(-90))
.overlay {
Text(1.0, format: .percent.precision(.fractionLength(1))) // invisible template view for Text sizing
.hidden()
.padding(.horizontal, min(viewSize.width, viewSize.height) * 0.1)
.frame(width: min(viewSize.width, viewSize.height))
.animatingPercent(for: progress)
.foregroundStyle(textStyle)
.lineLimit(1)
.font(.system(size: 250, weight: .bold, design: .rounded))
.minimumScaleFactor(0.01)
}
.background {
Circle()
.stroke(ringStyle, lineWidth: lineWidth)
.opacity(0.5)
.rotationEffect(.degrees(-90))
}
.animation(.easeOut, value: progress)
.readSize($into: $viewSize)
.padding()
}
}
// provide init parameters default values for ringStye = .tint and/or textStyle = .primary
extension CircularProgressView where RingStyle == TintShapeStyle, TextStyle == HierarchicalShapeStyle {
init(progress: Double) {
self.init(progress: progress, ringStyle: .tint, textStyle: .primary)
}
}
extension CircularProgressView where TextStyle == HierarchicalShapeStyle {
init(progress: Double, ringStyle: RingStyle) {
self.init(progress: progress, ringStyle: ringStyle, textStyle: .primary)
}
}
extension CircularProgressView where RingStyle == TintShapeStyle {
init(progress: Double, textStyle: TextStyle) {
self.init(progress: progress, ringStyle: .tint, textStyle: textStyle)
}
}
struct CircularProgressViewDemo: View {
@State private var progress = 0.0
var body: some View {
GeometryReader { proxy in
VStack(spacing: 0) {
let angularGradient = AngularGradient(colors: [.green, .yellow, .orange, .red, .purple, .blue, .green], center: .center)
let ellipticalGradient = EllipticalGradient(colors: [.green, .yellow, .orange, .red, .purple, .blue])
CircularProgressView(progress: progress, ringStyle: angularGradient, textStyle: ellipticalGradient)
.frame(height: proxy.size.height / 2.5)
HStack {
CircularProgressView(progress: progress, ringStyle: .red)
CircularProgressView(progress: progress, textStyle: .red)
}
.frame(height: proxy.size.height / 3)
HStack {
CircularProgressView(progress: progress)
CircularProgressView(progress: progress, ringStyle: .indigo)
CircularProgressView(progress: progress, textStyle: .green)
CircularProgressView(progress: progress, ringStyle: .orange, textStyle: .orange)
}
Slider(value: $progress, in: 0...1)
.padding()
Button {
progress = .random(in: 0...1)
} label: {
Label("Random", systemImage: "camera.filters")
.frame(maxWidth: .infinity, alignment: .center)
}
.padding(.horizontal)
.buttonStyle(.borderedProminent)
.controlSize(.large)
}
}
}
}
struct CircularProgressViewDemo_Previews: PreviewProvider {
static var previews: some View {
CircularProgressViewDemo()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment