How to animate a circle to fill instead of using .trim? - ios

I have this ring style view below, this animation starts at a filled ring of 100% and "unfills" to the trim amount, I'd like to reverse the animation so it starts at 0 and fills to the trim amount.
var progressAnimation: Animation {
Animation
.linear
.speed(0.5)
.delay(0.2)
}
var body: some View {
VStack {
ZStack {
Circle()
.stroke(Color(.systemFill).opacity(0.5), style: StrokeStyle(lineWidth: lineWidth))
.frame(height: 125)
Circle()
.trim(from: CGFloat(getTrimLevelForRingFromExertionLevel()), to: 1)
.stroke(ColorManager.getColorsForMetric(metricType: .Exertion(value: mostRecentEffortLevelObject?.effortLevel ?? 0)),
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round, miterLimit: .infinity, dash: [20, 0], dashPhase: 0))
.animation(self.progressAnimation, value: getTrimLevelForRingFromExertionLevel())
.rotationEffect(Angle(degrees: 90))
.rotation3DEffect(Angle(degrees: 180), axis: (x: 1, y: 0, z: 0))
.frame(height: 125)

Probably you have issue in different code or model, because approach in referenced answer works fine.
Here is a complete demo. Tested with Xcode 12.4 / iOS 14.4
struct DemoView: View {
#State private var progress = 0.0
var body: some View {
VStack {
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(Color.blue ,style: StrokeStyle(lineWidth: 25.0, lineCap: .round, lineJoin: .round))
.animation(.linear, value: progress)
.rotationEffect(Angle(degrees: 270.0))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
progress = 0.73
}
}
}.padding()
}
}

Related

Issue with automatic timed scroll view in SwiftUI [duplicate]

This question already has answers here:
How to scroll List programmatically in SwiftUI?
(10 answers)
Closed 8 months ago.
I am trying to make an automatic custom paged scroll view but I have run into an issue. I specified in my code for every 5 seconds to add an increment of 1 and go to the next page and organized text to each card with Identifiable. This is not happening in the preview. Instead, it is refusing to move and it is wanting to move only to the first card. When I put a fixed string value and remove the array that I have made, it works. I have tried to use other alternatives like an enum and a fixed array embedded in the view but none of that worked. Please review my code below…
struct TestView: View {
private var amount = 1
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
#State private var currentIndex = 0
var item: Item = items[0]
var body: some View {
ZStack {
ScrollViewReader { scrollProxy in
SnappingScrollView(.horizontal, decelerationRate: .fast, showsIndicators: false) {
ForEach(items) { item in
ForEach(0..<amount) { selection in
GeometryReader { geometry in
ZStack {
Rectangle()
.foregroundColor(Color.black.opacity(0.2))
.blur(radius: 7)
.frame(maxWidth: 350, maxHeight: 300, alignment: .center)
.cornerRadius(16)
.shadow(color: Color.black.opacity(0.2), radius: 20, x: 0, y: 0)
.offset(x: 15, y: 50)
.rotation3DEffect(Angle(degrees: (Double(geometry.frame(in: .global).minX) - 30) / -20), axis: (x: 0, y: 1.0, z: 0))
Rectangle()
.stroke(lineWidth: 50)
.foregroundColor(.gray)
.blur(radius: 100)
.frame(maxWidth: 350, maxHeight: 300, alignment: .center)
.cornerRadius(16)
.shadow(color: Color.black.opacity(0.3), radius: 1)
.offset(x: 15, y: 50)
.rotation3DEffect(Angle(degrees: (Double(geometry.frame(in: .global).minX) - 30) / -20), axis: (x: 0, y: 1.0, z: 0))
VStack {
Text(item.title)
.font(.title2)
.fontWeight(.bold)
.padding(.bottom, 10)
Text(item.description)
.font(.system(size: 16))
Image(item.image)
.resizable()
.frame(width: 90, height: 90)
.cornerRadius(25)
.padding(.bottom, -30)
.padding(.top)
Text("\(selection)")
.opacity(0)
}
.frame(width: 300)
.multilineTextAlignment(.center)
.offset(y: 20)
.rotation3DEffect(Angle(degrees: (Double(geometry.frame(in: .global).minX) - 30) / -20), axis: (x: 0, y: 1.0, z: 0))
.padding(.leading, 25)
.padding(.top, 70)
}
}
.frame(width: 400, height: 500)
.offset(x: 12)
.id(selection)
.scrollSnappingAnchor(.bounds)
}
}
}
.onReceive(timer) { _ in
currentIndex = currentIndex < amount-1 ? currentIndex + 1 : 0
withAnimation {
scrollProxy.scrollTo(currentIndex) // scroll to next .id
}
}
}
}
}
}
The Identifiable array
struct Item: Identifiable {
var id = UUID()
var title: String
var description: String
var image: String
}
var items = [
Item(title: "Welcome to Mathematically!", description: "It's a pleasure to have you. From creating a playground to competing in the daily challenges, there will be wonderful days of math waiting for you.", image: "Mathematically"),
Item(title: "An award winning app.", description: "Mathematically was submitted to the WWDC22 Swift Student Challenge and was accepted among the 350 accepted apps world-wide.", image: "WWDC22"),
Item(title: "Need a Walkthrough?", description: "No need to be intimidated. Take the walkthrough if you need to familiarize yourself about the game.", image: "Arrows"),
Item(title: "Mathematically+", description: "The ultimate premium Mathematically experience, ad-free.", image: "Mathematically+"),
]
You're almost there .. you just don't do anything with your currentIndex when it changes. Introduce a ScrollViewReader and an .id so you can scroll to the respective position in your view:
struct ContentView: View {
private let amount = 4
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
#State private var currentIndex = 0
var body: some View {
ScrollViewReader { scrollProxy in // needed to scroll programmatically
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 0) {
ForEach(0..<amount) { item in
GeometryReader { geometry in
ZStack {
Rectangle()
.foregroundColor(.gray)
.frame(maxWidth: 350, maxHeight: 300, alignment: .center)
.cornerRadius(16)
.offset(x: 15, y: 50)
.rotation3DEffect(Angle(degrees: (Double(geometry.frame(in: .global).minX) - 30) / -20), axis: (x: 0, y: 1.0, z: 0))
VStack {
Text("Hello World!")
.font(.title2)
.bold()
.padding(.bottom, 10)
Text("Item \(item)")
}
.frame(width: 300)
.rotation3DEffect(Angle(degrees: (Double(geometry.frame(in: .global).minX) - 30) / -20), axis: (x: 0, y: 1.0, z: 0))
.padding(.leading, 25)
.padding(.top, 70)
}
}
.frame(width: 400, height: 500)
.offset(x: 10)
.id(item) // define id here
}
}
}
.onReceive(timer) { _ in
currentIndex = currentIndex < amount-1 ? currentIndex + 1 : 0
withAnimation {
scrollProxy.scrollTo(currentIndex) // scroll to next .id
}
}
}
}
}

iOS SwiftUI - How to stick a label to a rotating view without the label rotating too?

This is the desired outcome
This is what I have now
Can anyone help? I'm new to SwiftUI and I've been struggling for two days
The thin line and the rotation works well, but how can I keep the label horizontal at any rotation?
I have tried using a VSTack and that causes undesired behavior. And when I set the rotation only to the rectangle (thin line) I can't figure out how to correctly postion the label dynamically.
This is my code so far, and the piece at TodayLabel is where this is done
struct SingleRingProgressView: View {
let startAngle: Double = 270
let progress: Float // 0 - 1
let ringWidth: CGFloat
let size: CGFloat
let trackColor: Color
let ringColor: Color
let centerText: AttributedText?
let centerTextSubtitle: AttributedText?
let todayLabel: CircleGraph.Label?
private let maxProgress: Float = 2 // allows the ring show a progress up to 200%
private let shadowOffsetMultiplier: CGFloat = 4
private var absolutePercentageAngle: Float {
percentToAngle(percent: (progress * 100), startAngle: 0)
}
private var relativePercentageAngle: Float {
// Take into account the startAngle
absolutePercentageAngle + Float(startAngle)
}
#State var position: (x: CGFloat, y: CGFloat) = (x: 0, y: 0)
var body: some View {
GeometryReader { proxy in
HStack {
Spacer()
VStack {
Spacer()
ZStack {
Circle()
.stroke(lineWidth: ringWidth)
.foregroundColor(trackColor)
.frame(width: size, height: size)
Circle()
.trim(from: 0.0, to: CGFloat(min(progress, maxProgress)))
.stroke(style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(ringColor)
.rotationEffect(Angle(degrees: startAngle))
.frame(width: size, height: size)
if shouldShowShadow(frame: proxy.size) {
Circle()
.fill(ringColor)
.frame(width: ringWidth, height: ringWidth, alignment: .center)
.offset(y: -(size/2))
.rotationEffect(Angle.degrees(360 * Double(progress)))
.shadow(
color: Color.white,
radius: 2,
x: endCircleShadowOffset().0,
y: endCircleShadowOffset().1)
.shadow(
color: Color.black.opacity(0.5),
radius: 1,
x: endCircleShadowOffset().0,
y: endCircleShadowOffset().1)
}
// Today label
if let todayLabel = self.todayLabel {
ZStack {
StyledText(todayLabel.label)
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(Color.color(token: .hint))
.cornerRadius(2)
.offset(y: -(size/1.5))
Rectangle()
.frame(width: 2, height: ringWidth + 2, alignment: .center)
.offset(y: -(size/2))
}.rotationEffect(Angle.degrees(Double(todayLabel.degrees)))
}
VStack(spacing: 4) {
if let text = centerText {
StyledText(text)
}
if let subtitle = centerTextSubtitle {
StyledText(subtitle)
.frame(maxWidth: 120)
.multilineTextAlignment(.center)
}
}
}
Spacer()
}
Spacer()
}
}
}
private func percentToAngle(percent: Float, startAngle: Float) -> Float {
(percent / 100 * 360) + startAngle
}
private func endCircleShadowOffset() -> (CGFloat, CGFloat) {
let angleForOffset = absolutePercentageAngle + Float(startAngle + 90)
let angleForOffsetInRadians = angleForOffset.toRadians()
let relativeXOffset = cos(angleForOffsetInRadians)
let relativeYOffset = sin(angleForOffsetInRadians)
let xOffset = CGFloat(relativeXOffset) * shadowOffsetMultiplier
let yOffset = CGFloat(relativeYOffset) * shadowOffsetMultiplier
return (xOffset, yOffset)
}
private func shouldShowShadow(frame: CGSize) -> Bool {
let circleRadius = min(frame.width, frame.height) / 2
let remainingAngleInRadians = CGFloat((360 - absolutePercentageAngle).toRadians())
if (progress * 100) >= 100 {
return true
} else if circleRadius * remainingAngleInRadians <= ringWidth {
return true
}
return false
}
}
just turn the inner text label back by -angle:
struct ContentView: View {
let startAngle: Double = 270
let progress: Float = 0.2 // 0 - 1
let ringWidth: CGFloat = 30
let size: CGFloat = 200
let trackColor: Color = .gray
let ringColor: Color = .blue
let todayLabeldegrees = 120.0
#State var position: (x: CGFloat, y: CGFloat) = (x: 0, y: 0)
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: ringWidth)
.foregroundColor(trackColor)
.frame(width: size, height: size)
Circle()
.trim(from: 0.0, to: CGFloat(progress))
.stroke(style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(ringColor)
.rotationEffect(Angle(degrees: startAngle))
.frame(width: size, height: size)
// Today label
ZStack {
Text("todayLabel")
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(Color.white)
.cornerRadius(5)
.shadow(radius: 2)
.rotationEffect(Angle.degrees(-todayLabeldegrees)) // << turn back
.offset(y: -(size/1.5))
Rectangle()
.frame(width: 2, height: ringWidth + 2, alignment: .center)
.offset(y: -(size/2))
}
.rotationEffect(Angle.degrees(todayLabeldegrees))
VStack(spacing: 4) {
Text("Test").font(.title)
Text("subtitle")
.frame(maxWidth: 120)
.multilineTextAlignment(.center)
}
}
}
}

Framming trimmed circle view

I'm fairly new to SwiftUI (worked with UIKit before), and I got this internal, learning/testing project to do, and I ran into this problem.
I have two trimmed circles in a ZStack (and those two texts), in a HStack so I can add that little icon on its right side. How can I trim that bottom, invisible part of the circles so it fits my needs because it pushes everything down?
Edit: I tried clipping and padding with EdgeInsets, but still, changing the frame size pushes everything around. I'm looking for a proper way of doing this, and not some "hack".
screenshot
private struct ActivityCircleComponent: View {
private let widthOfCircle: CGFloat = 15
private let headerIconSize = UIScreen.main.bounds.height * 0.03
private let rotationAmount: Double = 152.5
private let progress: Double = 0.4
private let trimAmount: CGFloat = 40
var body: some View {
GeometryReader { g in
HStack(alignment: .bottom) {
ZStack {
ZStack(alignment: .center) {
Circle()
.trim(from: 0.0, to: 0.65)
.stroke(style: StrokeStyle(lineWidth: self.widthOfCircle, lineCap: .round, lineJoin: .round))
.foregroundColor(.black)
.rotationEffect(Angle(degrees: self.rotationAmount))
Circle()
.trim(from: 0.0, to: self.progress)
.stroke(style: StrokeStyle(lineWidth: self.widthOfCircle, lineCap: .round, lineJoin: .round))
.foregroundColor(.red)
.rotationEffect(Angle(degrees: self.rotationAmount))
}
.padding([.leading, .top, .trailing], self.widthOfCircle / 2)
VStack {
Text("10.000")
.font(Fonts.Roboto.bold.of(size: 30))
Text("meters")
.font(Fonts.Roboto.light.of(size: 20))
}
}
Image("Goal")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: self.headerIconSize)
}
.frame(width: g.size.width, height: g.size.height)
}
}
}
PS Don't mind the state of the code, I'm just playing around
Thanks a bunch!
Here is how you would use the link I provided in my comment. I used .overlay instead of ZStack as you get one view that you don't have to worry about tweaking things when your screen size changes. Easy peasy:
struct ActivityCircleComponent: View {
private let widthOfCircle: CGFloat = 15
private let headerIconSize = UIScreen.main.bounds.height * 0.03
private let progress: Double = 0.4
private let trimAmount: CGFloat = 40
var body: some View {
HStack(alignment: .bottom) {
Arc(startAngle: .degrees(0), endAngle: .degrees(220), clockwise: true)
.stroke(style: StrokeStyle(lineWidth: widthOfCircle, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 120)
.fixedSize()
.overlay(
Arc(startAngle: .degrees(0), endAngle: .degrees(progress * 220), clockwise: true)
.stroke(style: StrokeStyle(lineWidth: widthOfCircle, lineCap: .round, lineJoin: .round))
.foregroundColor(.red)
.frame(width: 300, height: 120)
.fixedSize()
)
Image(systemName: "rosette")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: self.headerIconSize)
}
.overlay(
VStack {
Text("10.000")
Text("meters")
}
)
}
}
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
func path(in rect: CGRect) -> Path {
let rotationAdjustment = Angle.degrees(200)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise)
return path
}
}

Updating Path in SwiftUI

I have got a SwiftUI View with a chart which is made using Path and data from entries array.
The code:
struct TodayChartView: View {
var body: some View {
GeometryReader { reader in
Path { p in
for i in 1..<entries.count {
p.move(to: CGPoint(x: entries[i-1].x, entries[i-1].y: y))
p.addLine(to: CGPoint(entries[i].x: x, entries[i].y: y))
}
}
}.stroke(Color.blue, style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
}
}
When user presses the button in another View, a new entry is added to entries and I want to display it on my Path. So, basically, I need to somehow trigger redrawing it. Is there any way to achieve it?
Pass the entries as a parameter to TodayChartView. With the entries as a #State var in ContentView, the TodayChartView gets redrawn whenever entries changes.
Here is a complete example (tested in Xcode 11.7):
struct TodayChartView: View {
let entries: [CGPoint]
var body: some View {
// GeometryReader { reader in
Path { p in
for i in 1..<self.entries.count {
p.move(to: CGPoint(x: self.entries[i-1].x, y: self.entries[i-1].y))
p.addLine(to: CGPoint(x: self.entries[i].x, y: self.entries[i].y))
}
}
.stroke(Color.blue, style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
// }
}
}
struct ContentView: View {
#State private var entries: [CGPoint] = [CGPoint(x: 150, y: 150)]
var body: some View {
VStack(spacing: 20) {
Button("Add Point") {
let x = CGFloat.random(in: 0...300)
let y = CGFloat.random(in: 0...300)
self.entries.append(CGPoint(x: x, y: y))
}
TodayChartView(entries: self.entries)
.frame(width: 300, height: 300)
}
}
}
Note: Your TodayChartView is not currently using GeometryReader so I've commented it out.
Try this one
Path { p in
for i in 1..<entries.count {
p.move(to: CGPoint(x: entries[i-1].x, entries[i-1].y: y))
p.addLine(to: CGPoint(entries[i].x: x, entries[i].y: y))
}
}
}.stroke(Color.blue, style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
.id(entries) // << here !!

SwiftUI animation: How to stagger repeating animation with delay

Trying to implement the following animation in SwiftUI and finding it quite impossible:
To summarize the effect: A repeating pulse, animating each segment with a staggered delay. For each segment:
Starting at opacity=0.5, scale = 1
Animate to opacity=1.0, scale = 1.3
The animation curve is starts quickly with a longer ease-out
In addition, there is delay between each 'pulse'.
The closest I've been able to get to this with SwiftUI is a continuously repeating animation using the .repeatForever modifier. (Code below. Ignore the mismatch in timing for now.)
How can I Add a delay between each loop of the animation?
Here is the result of my code below:
import SwiftUI
struct ArrowShape : Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height/2.0))
path.addLine(to: CGPoint(x: 0, y: rect.size.height))
return path
}
}
struct Arrows: View {
private let arrowCount = 3
#State var scale:CGFloat = 1.0
#State var fade:Double = 0.5
var body: some View {
ZStack {
Color(red: 29.0/255.0, green: 161.0/255.0, blue: 224.0/255.0).edgesIgnoringSafeArea(.all)
HStack{
ForEach(0..<self.arrowCount) { i in
ArrowShape()
.stroke(style: StrokeStyle(lineWidth: CGFloat(10),
lineCap: .round,
lineJoin: .round ))
.foregroundColor(Color.white)
.aspectRatio(CGSize(width: 28, height: 70), contentMode: .fit)
.frame(maxWidth: 20)
.animation(nil)
.opacity(self.fade)
.scaleEffect(self.scale)
.animation(
Animation.easeOut(duration: 0.8)
.repeatForever(autoreverses: true)
.delay(0.2 * Double(i))
)
}
}
.onAppear() {
self.scale = 1.2
self.fade = 1.0
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Arrows()
}
}
I think you can implement it using Timer and DispatchQueue, try this and see it's working as you want or no
struct Arrows: View {
private let arrowCount = 3
let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
#State var scale:CGFloat = 1.0
#State var fade:Double = 0.5
var body: some View {
ZStack {
Color(red: 29.0/255.0, green: 161.0/255.0, blue: 224.0/255.0).edgesIgnoringSafeArea(.all)
HStack{
ForEach(0..<self.arrowCount) { i in
ArrowShape()
.stroke(style: StrokeStyle(lineWidth: CGFloat(10),
lineCap: .round,
lineJoin: .round ))
.foregroundColor(Color.white)
.aspectRatio(CGSize(width: 28, height: 70), contentMode: .fit)
.frame(maxWidth: 20)
.animation(nil)
.opacity(self.fade)
.scaleEffect(self.scale)
.animation(
Animation.easeOut(duration: 0.5)
//.repeatForever(autoreverses: true)
.repeatCount(1, autoreverses: true)
.delay(0.2 * Double(i))
)
}.onReceive(self.timer) { _ in
self.scale = self.scale > 1 ? 1 : 1.2
self.fade = self.fade > 0.5 ? 0.5 : 1.0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.scale = 1
self.fade = 0.5
}
}
}
}
}
}

Resources