SwiftUI, Circular progress bar with image on progress - swiftui

I'm trying to implement a circular progress bar in SwiftUI with an image on the progress and animation. Any help or idea would be appreciated.
Here is what I plan to implement:imageLink
This is what I could implement so far: image2
And this is my code:
struct CircularView: View {
#State var progress:Double = 0
var total:Double = 100
let circleHeight:CGFloat = 217
#State var xPos:CGFloat = 0.0
#State var yPos:CGFloat = 0.0
var body: some View {
let pinHeight = circleHeight * 0.1
VStack {
Circle()
.trim(from: 0.0, to: 0.6)
.stroke(Color(.systemGray5),style: StrokeStyle(lineWidth: 8.0, lineCap: .round, dash: [0.1]))
.frame(width: 278, height: 217, alignment: .center)
.rotationEffect(.init(degrees: 162))
.overlay(
Circle()
.trim(from: 0.0, to: CGFloat(progress) / CGFloat(total))
.stroke(Color.purple,style: StrokeStyle(lineWidth: 8.0, lineCap: .round, dash: [0.1]))
.rotationEffect(.init(degrees: 162))
.rotation3DEffect(
.init(degrees: 1),
axis: (x: 1.0, y: 0.0, z: 0.0)
)
.animation(.easeOut)
.overlay(
Circle()
.frame(width: pinHeight, height: pinHeight)
.foregroundColor(.blue)
.offset(x: 107 - xPos, y: 0 + yPos)
.animation(.easeInOut)
.rotationEffect(.init(degrees: 162))
)
)
.foregroundColor(.red)
Button(action: {
progress += 5
if progress >= 65 {
progress = 0
}
}, label: {
Text("Button")
})
}
}
}

Without changing much of your initial code, the idea is to rotate also the image when the circular progress bar value changes
Your code would look like this:
struct CircularView: View {
#State var progress:Double = 0
var total:Double = 100
let circleHeight:CGFloat = 217
#State var xPos:CGFloat = 0.0
#State var yPos:CGFloat = 0.0
var body: some View {
let pinHeight = circleHeight * 0.1
VStack {
Circle()
.trim(from: 0.0, to: 0.6)
.stroke(Color(.systemGray5),style: StrokeStyle(lineWidth: 8.0, lineCap: .round, dash: [0.1]))
.frame(width: 278, height: 217, alignment: .center)
.rotationEffect(.init(degrees: 162))
.overlay(
Circle()
.trim(from: 0.0, to: CGFloat(progress) / CGFloat(total))
.stroke(Color.purple,style: StrokeStyle(lineWidth: 8.0, lineCap: .round, dash: [0.1]))
.rotationEffect(.init(degrees: 162))
.rotation3DEffect(
.init(degrees: 1),
axis: (x: 1.0, y: 0.0, z: 0.0)
)
// .animation(.easeOut)
.overlay(
Circle()
.frame(width: pinHeight, height: pinHeight)
.foregroundColor(.blue)
.offset(x: 107 - xPos, y: 0 + yPos)
// .animation(.easeInOut)
.rotationEffect(Angle(degrees: progress * 3.6))
.rotationEffect(.init(degrees: 162))
)
)
.foregroundColor(.red)
.animation(.easeOut) // Animation added here
Button(action: {
progress += 5
if progress >= 65 {
progress = 0
}
}, label: {
Text("Button")
})
}
}
}
1. Apply one common animation to have a better transition
I Have removed .animation modifiers in 2 places and place only one on the parent Circle View to allow a smooth animation ( You can still change and adapt it to the output you desire )
2. Calculate Image Angle rotation Degree
Perform simple calculation to determine rotation Angle, how much degree should I rotate the Image view
So, basically it is: .rotationEffect(Angle(degrees: (progress / 60) * (0.6 * 360)) => .rotationEffect(Angle(degrees: progress * 3.6))

Related

How do I add transitions between switch result cases?

Currently, my code uses a series of switch result cases that asks questions and presents different answer choices. For example, when the user clicks Evening, the code will present a new question with the answer choices being "Sweet" and "Savory". How can I put a cool transition where the "Morning" and "Evening" choices turn into the "Sweet" and "Savory" choices? Open to any transitions that you'd recommend.
var body: some View {
let cardWidth = 300
let cardWidthValue = CGFloat(cardWidth)
let cardHeight = 300
let cardHeightValue = CGFloat(cardHeight)
VStack {
ZStack {
Rectangle()
.fill(Color(red: 0.0, green: 0.5, blue: 1.0, opacity: 0.4))
.frame(width: 350, height: 250)
.cornerRadius(20)
.shadow(color: .gray, radius: 20)
switch result {
case "Start":
VStack{
Text("What time is it?")
.padding()
.foregroundColor(Color.white)
.font(.system(size: 30.0, weight: .bold))
HStack {
Button {
result = "M"
} label: {
Text("Morning🌥")
.foregroundColor(Color.white)
.frame(width: 165, height: 100)
.background(Color(red: 0.0, green: 0.0, blue: 1.0, opacity: 1.0))
.cornerRadius(10)
}
Button {
result = "E"
} label: {
Text("Evening☀️")
.foregroundColor(Color.white)
.frame(width: 165, height: 100)
.background(Color(red: 0.0, green: 0.0, blue: 1.0, opacity: 1.0))
.cornerRadius(10)
}
}
}
case "E":
VStack{
Text("Sweet or Savory?")
.padding()
.foregroundColor(Color.white)
.font(.system(size: 30.0, weight: .bold))
HStack {
Button {
result = "EveningSweet"
} label: {
Text("Sweet🍦")
}
.foregroundColor(Color.white)
.frame(width: 165, height: 100)
.background(Color(red: 0.0, green: 0.0, blue: 1.0, opacity: 1.0))
.cornerRadius(10)
Button {
result = "EveningSavory"
} label: {
Text("Savory🧂")
}
.foregroundColor(Color.white)
.frame(width: 165, height: 100)
.background(Color(red: 0.0, green: 0.0, blue: 1.0, opacity: 1.0))
.cornerRadius(10)
}
}
Using withAnimation{} to animate your event.
Let's say you want to animate views after you click on Evening:
Button {
withAnimation { //here
result = "E"
}
} label: {
Text("Evening☀️")
.foregroundColor(Color.white)
.frame(width: 165, height: 100)
.background(Color(red: 0.0, green: 0.0, blue: 1.0, opacity: 1.0))
.cornerRadius(10)
}
This mean that all views that are related to this event of result = "E" shall be animated. The default transition of all animation is .opacity.
After trying the first case, if you want to change transition type, use .transition()
case "E":
VStack {
}.transition(.slide)
You will see a different animation from the first time that you did not set transition equals to slide.
I would recommend you try the first option without .slide first to see the difference.

SwiftUI: Countdown Circle with Animation

I've created a progress circle for my App, that will count down from 28 to 0 (number of days until Payday)
I've got the circles to display how I'd like, but I want it to animate when I navigate to the view, is there a way to do this?
More a UI question, but do you think maybe an arrow within the circle to indicate the direction would look any good?
P.s - any ideas how to truncate all the zeros off my Double?
image of how this looks
Thanks!
ZStack {
let progressPeriod = Double(PayData.daysUntilPay) ?? 0
let progressPeriod2 = 1 - (progressPeriod / 28)
Circle()
.stroke(lineWidth: 30.0)
.opacity(0.3)
.foregroundColor(Color(red: 0.941, green: 0.426, blue: 0.004))
.frame(width:150)
.frame(height: 200)
Circle()
.trim(from: 0.0, to: progressPeriod2)
.stroke(style: StrokeStyle(lineWidth: 30.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color(red: 0.941, green: 0.426, blue: 0.004))
.rotationEffect(Angle(degrees: 270.0))
.frame(width:150)
.frame(height: 200)
Text("\(progressPeriod) days")
.bold()
}
Try to combine following code with your countdown:
struct ContentView: View {
#State var progressValue: Float = 0.0
var body: some View {
ZStack {
Color.yellow
.opacity(0.1)
.edgesIgnoringSafeArea(.all)
VStack {
ProgressBar(progress: self.$progressValue)
.frame(width: 150.0, height: 150.0)
.padding(40.0)
Button(action: {
self.incrementProgress()
}) {
HStack {
Image(systemName: "plus.rectangle.fill")
Text("Increment")
}
.padding(15.0)
.overlay(
RoundedRectangle(cornerRadius: 15.0)
.stroke(lineWidth: 2.0)
)
}
Spacer()
}
}
}
func incrementProgress() {
let randomValue = Float([0.012, 0.022, 0.034, 0.016, 0.11].randomElement()!)
self.progressValue += randomValue
}
}
struct ProgressBar: View {
#Binding var progress: Float
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.red)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.red)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear)
Text(String(format: "%.0f %%", min(self.progress, 1.0)*100.0))
.font(.largeTitle)
.bold()
}
}
}

SwiftUI Circle View animation glitch

import SwiftUI
struct CircularProgressView: View {
#Binding var progress: Float
private let strokeStyle = StrokeStyle(lineWidth: 30.0, lineCap: .round, lineJoin: .round)
private let rotation = Angle(degrees: 270.0)
var body: some View {
ZStack {
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: strokeStyle)
.opacity(0.2)
.foregroundColor(Color.black)
.rotationEffect(rotation)
.offset(x: 0, y: 10)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: strokeStyle)
.opacity(1)
.foregroundColor(Color.white)
.rotationEffect(rotation)
}
.animation(.linear(), value: progress)
}
}
struct CircularProgressView_Previews: PreviewProvider {
static var previews: some View {
OtherView(progress: 0.6)
}
struct OtherView : View {
#State var progress : Float = 0.0
var body: some View {
ZStack {
Color.yellow
VStack {
CircularProgressView(progress: self.$progress)
.padding(50)
Button(action: {
if(progress >= 1) {
progress = 0
} else {
progress += 0.1
}
}) {
Text("try me")
.frame(width: 200, height: 50)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.blue, lineWidth: 2)
)
.padding(.bottom, 100)
}
}
}.ignoresSafeArea()
}
}
}
What could be the reason?
It is unclear definitely, probably due to undefined size of shapes as a nature... Anyway, seems using drawing group fixes the issue.
Here is a fixed code. Tested with Xcode 13 / iOS 15.
struct CircularProgressView: View {
#Binding var progress: Float
private let strokeStyle = StrokeStyle(lineWidth: 30.0, lineCap: .round, lineJoin: .round)
private let rotation = Angle(degrees: 270.0)
var body: some View {
ZStack {
Group {
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: strokeStyle)
.opacity(0.2)
.foregroundColor(Color.black)
.rotationEffect(rotation)
.offset(x: 0, y: 10)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: strokeStyle)
.opacity(1)
.foregroundColor(Color.white)
.rotationEffect(rotation)
}
.padding() // << compensate offset within own bounds
}
.drawingGroup() // << here !!
.animation(.linear, value: progress)
}
}
struct CircularProgressView_Previews: PreviewProvider {
static var previews: some View {
OtherView(progress: 0.6)
}
struct OtherView : View {
#State var progress : Float = 0.0
var body: some View {
ZStack {
Color.yellow
VStack {
CircularProgressView(progress: self.$progress)
.padding()
Button(action: {
if(progress >= 1) {
progress = 0
} else {
progress += 0.1
}
}) {
Text("try me")
.frame(width: 200, height: 50)
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.blue, lineWidth: 2)
)
.padding(.bottom, 100)
}
}
}.ignoresSafeArea()
}
}
}

How to add icon to circle progress bar tip?

I'm trying to add an icon to the tip of the progress bar in the circle. This is the code I have so far:
Circle()
.trim(from: 0, to: 0.6)
.stroke(style: StrokeStyle(lineWidth: 24, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(-90))
.foregroundColor(.green)
.frame(width: 300, height: 300)
.padding()
This ends up looking like this:
But I'm trying to add an icon to the tip of the progress like this:
How can I achieve this in SwiftUI?
Use the left arrow icon and put it into the ZStack and set y offset in minus.
struct DemoView: View {
#State private var angle: Double = -90
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.6)
.stroke(style: StrokeStyle(lineWidth: 24, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(angle))
.foregroundColor(.green)
Image("left-arrow")
.offset(y: -150)
.rotationEffect(.degrees(305 + angle))
}.frame(width: 300, height: 300)
.padding()
}
}
I have added an image arrow using progress code from here
struct ProgressBar: View {
#Binding var progress: Float
var body: some View {
GeometryReader { geo in
ZStack(alignment: .center) {
Circle()
.stroke(lineWidth: 20.0)
.opacity(0.3)
.foregroundColor(Color.red)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.red)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear)
Text(String(format: "%.0f %%", min(self.progress, 1.0)*100.0))
.font(.largeTitle)
.bold()
Image("left-arrow")
.offset(y: -(geo.size.height/2))
.rotationEffect(.degrees(Double(self.progress) * 360))
.animation(.linear)
}
}
}
}

Using geometry reader to Center the Rectangle

I'm trying to use GeometryReader to make a Card View (to use in a card game).
This Card is going to have 3 Shapes on it.
I'm trying to center these 3 Shapes using GeometryReader (starting with the first shape, Rectangle), but it doesn't work.
What am I doing wrong?
Here's how I WANT it to look like: Here's how I want it to look like
Here's how it ACTUALLY looks like: Here's how it actually looks like
struct Card: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10.0).stroke(lineWidth: 3)
VStack {
GeometryReader { geometry in
Rectangle()
.size(
width: geometry.size.width * 0.75,
height: geometry.size.height * 0.75
)
.position(
x: geometry.size.width / 2,
y: geometry.size.height / 2
)
}
GeometryReader { geometry in
Circle()
.size(
width: geometry.size.width * 0.75,
height: geometry.size.height * 0.75
)
.position(
x: geometry.size.width / 2,
y: geometry.size.height / 2
)
}
GeometryReader { geometry in
Rectangle()
.size(
width: geometry.size.width * 0.75,
height: geometry.size.height * 0.75
)
.position(
x: geometry.size.width / 2,
y: geometry.size.height / 2
)
}
}
}
.foregroundColor(Color.orange)
}
}
Here is possible variant of layout. Tested with Xcode 12 / iOS 14
struct Card3: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10.0).stroke(lineWidth: 3)
GeometryReader { geometry in
VStack {
Color.clear.overlay(Rectangle()
.frame(
width: geometry.size.width * 0.75,
height: geometry.size.height / 3 * 0.75
))
Color.clear.overlay(Circle()
.frame(
width: geometry.size.width * 0.75,
height: geometry.size.height / 3 * 0.75
))
Color.clear.overlay(Rectangle()
.frame(
width: geometry.size.width * 0.75,
height: geometry.size.height / 3 * 0.75
))
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.foregroundColor(Color.orange)
}
}