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()
}
}
}
Related
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()
}
}
}
After trimming a Circle(), its borders lose smoothness.
struct WidgetsEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
ZStack {
Circle()
.trim(from: 0.0, to: min(CGFloat(0.5), 1.0))
.stroke(Color.green, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.rotationEffect(Angle(degrees: 270))
}
Text("Subtitle")
.foregroundColor(.white)
}
.padding()
.background(Color.black)
}
}
If you remove the padding, text, or the trimming itself anti-aliasing returned.
The problem is I don't understand how to fix my Content View to the screen and handle things like rotation or even how to make other screen sizes dynamic. Below is my entire View code. When I create a new app out of the box it looks fine in preview, the way I like it. However, screen sizes change and all I know to do is put magic numbers, because there is no superview or frame to size from. Also I can't seem to clip at screen edges. So my background view is way to big. How is this handled in SwiftUI?
import SwiftUI
import AuthenticationServices
struct LoginScreen: View {
var body: some View {
ZStack{
backgroundLayout
loginLayout
}
}
var loginLayout: some View {
VStack {
welcomeText
signInWithAppleButton
}
}
var backgroundLayout: some View {
ZStack{
Rectangle()
.foregroundColor(.init(.sRGB, red: 0, green: 0.750, blue: 0.750, opacity: 0.25))
Circle()
.foregroundColor(.blue)
.offset(x: 200, y: -200)
.aspectRatio(1/3, contentMode: .fill)
.clipped()
Circle()
.foregroundColor(.green)
.offset(x: -200, y: 200)
.aspectRatio(1/3, contentMode: .fill)
.clipped()
}
}
var welcomeText: some View {
HStack {
VStack {
Text("Sign Up")
.font(.largeTitle)
.bold()
Text("Sign In")
.font(.title)
.bold()
Text("Get Started!")
.font(.title2)
.bold()
}
.offset(x: -100, y: -150)
}
}
var signInWithAppleButton: some View {
SignInWithAppleButton(
.continue,
onRequest: { request in
request.requestedScopes = [.fullName, .email]
},
onCompletion: { result in
switch result {
case .success (let authenticationResults):
print("Authorization successful! :\(authenticationResults)")
case .failure(let error):
print("Authorization failed: " + error.localizedDescription)
}
}
)
.offset(x: 0, y: 150)
.frame(width: 200, height: 50, alignment: .center)
}
}
struct Login_Preview: PreviewProvider {
static var previews: some View {
LoginScreen()
}
}
After trying several things this is all I could come up with. It's not perfect and certainly not ideal, but at least it work somewhat as expected.
import SwiftUI
import AuthenticationServices
struct LoginScreen: View {
var body: some View {
loginLayout.background(backgroundLayout)
.statusBar(hidden: true)
}
var loginLayout: some View {
GeometryReader {geo in
VStack {
welcomeText.padding(.top, geo.size.height / 10)
Spacer()
signInWithAppleButton
.padding(.bottom, geo.size.height / 10)
}
}
}
var backgroundLayout: some View {
GeometryReader { geo in
let circleSize = min(geo.size.width, geo.size.height)
ZStack {
Rectangle()
.foregroundColor(.init(.sRGB, red: 0, green: 0.750, blue: 0.750, opacity: 0.25))
Circle()
.foregroundColor(.blue)
.offset(x: circleSize / 1.75,
y: -geo.size.height / 2.5)
.frame(width: circleSize, height: circleSize, alignment: .center)
Circle()
.foregroundColor(.green)
.offset(x: -circleSize / 1.75, y: geo.size.height / 2.5)
.frame(width: circleSize, height: circleSize, alignment: .center)
}
.frame(width: geo.size.width, height: geo.size.height, alignment: .top)
}
}
var welcomeText: some View {
GeometryReader { geo in
HStack {
VStack {
Text("Sign Up")
.font(.largeTitle)
.bold()
Text("Sign In")
.font(.title)
.bold()
Text("Get Started!")
.font(.title2)
.bold()
}
.padding(.leading, geo.size.width / 8)
Spacer()
}
}
}
var signInWithAppleButton: some View {
SignInWithAppleButton(
// Add this below for Continue with Apple button
.continue,
onRequest: { request in
request.requestedScopes = [.fullName, .email]
},
onCompletion: { result in
switch result {
case .success (let authenticationResults):
print("Authorization successful! :\(authenticationResults)")
case .failure(let error):
print("Authorization failed: " + error.localizedDescription)
}
}
)
.frame(width: 200, height: 50, alignment: .center)
}
}
struct Login_Preview: PreviewProvider {
static var previews: some View {
LoginScreen()
}
}
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)
}
}
}
}
I'm trying to get the last styling done on my Widget but I just can't seem to get the number "+1.75" and the Orange dot to align with the "Wheat" text. How do I properly align this row?
I've tried using Spacer() but it just doesn't seem to work?
struct CropPriceView: View {
#Environment(\.widgetFamily) var family: WidgetFamily
var body: some View {
if family == .systemSmall{
VStack{
HStack{
VStack{
HStack{
Circle()
.fill(Color(#colorLiteral(red: 0.9333333333, green: 0.6784313725, blue: 0.262745098, alpha: 1)))
.frame(width: 10, height: 10)
VStack(alignment: .leading){
Text("Wheat")
.foregroundColor(Color(#colorLiteral(red: 0.168627451, green: 0.1647058824, blue: 0.1647058824, alpha: 1)))
.font(Font.custom("TPFrank-Bold", size: 14))
.lineSpacing(0.18)
Text("MATIF")
.font(Font.custom("TPFrank-Regular", size: 12))
.foregroundColor(Color(#colorLiteral(red: 0.5921568627, green: 0.5921568627, blue: 0.5921568627, alpha: 1)))
.lineSpacing(0.15)
}
Spacer()
}
}
Spacer()
Text("+1.75")
.foregroundColor(Color(#colorLiteral(red: 0.4431372549, green: 0.7490196078, blue: 0.3882352941, alpha: 1)))
.font(Font.custom("TPFrank-Medium", size: 14))
.lineSpacing(0.35)
}
Spacer()
HStack{
Spacer()
Text("168.02")
.foregroundColor(Color(#colorLiteral(red: 0.168627451, green: 0.1647058824, blue: 0.1647058824, alpha: 1)))
.font(Font.custom("TPFrank-Bold", size: 22))
.lineSpacing(0.55)
}
}.padding(EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15))
}
}
}
Hopefully this helps, let me know if it works for you. I believe you were looking for .firstTextBaseline to align with the first Text.
struct Example_WidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "circle.fill")
.resizable()
.frame(width: 10, height:10)
.foregroundColor(Color.orange)
VStack (alignment: .leading){
Text("Wheat")
.font(.subheadline)
.fontWeight(.bold)
.allowsTightening(true)
Text("MATIF")
.font(.footnote)
.fontWeight(.bold)
.foregroundColor(Color.secondary)
.allowsTightening(true)
}
Spacer()
Text("+1.75")
.font(.footnote)
.fontWeight(.bold)
.foregroundColor(Color.green)
.allowsTightening(true)
}
Spacer()
HStack {
Spacer()
Text("168.02")
.font(.title)
.fontWeight(.bold)
}
}
.padding(.all, 10)
}
}
Set HStack alignment: .top
var body: some View {
if family == .systemSmall{
VStack{
HStack(alignment: .top) { //<<--Here
VStack{
HStack(alignment: .top) { //<<--Here