In xCode 11.4 the circle animation was behaving correctly. It was causing a delay in the progress ring (the circle inside the border) a few seconds after the bottom sheet was presented. Now it is delaying the presentation of the entire circle in Xcode 13.2.
Here is full code example with a video example:
https://github.com/SimonSays1993/SwiftUI_Part2/blob/main/README.md
In a short summary, this is what's currently happening.
In another view, there is a card, and when I tap on it I toggle a state which displays a bottom sheet. This state then gets passed to the BottomSheetView and then to RingView through Binding.
We then use this value to display the RingView. The RingView has a delay animation base on the Binding variable show. This works fine in presenting the CircleView, but the problem is when the RingView appears I toggle a state to become true to then try and start the animation of the second Circle (call this progress ring) inside the border view of the circle.
Every time the RingView appears the progress ring is already loaded, and its delayed animation is not working.
struct RingView: View {
var color1 = Color.red
var color2 = Color.purple
var width: CGFloat = 88
var height: CGFloat = 88
var percent: CGFloat = 88
#Binding var show: Bool
#State var progressCircle: Bool = false
var body: some View {
let multiplier = width / 44
let progress = 1 - (percent / 100)
return ZStack {
//The grey border circle
Circle()
.stroke(Color.black.opacity(0.1), style: StrokeStyle(lineWidth: 5 * multiplier))
.frame(width: width, height: height)
Circle()
.trim(from: progressCircle ? progress : 1, to: 1)
.stroke(style: StrokeStyle(lineWidth: 5 * multiplier, lineCap: .round))
.rotationEffect(Angle(degrees: 90))
.rotation3DEffect(Angle(degrees: 180), axis: (x: 1, y: 0, z: 0))
.frame(width: width, height: height)
.animation(.linear.delay(2.0), value: progressCircle)
Text("\(Int(percent))%")
.font(.system(size: 14 * multiplier))
.fontWeight(.bold)
}
.animation(.linear.delay(1.5), value: show)
.onAppear {
self.progressCircle.toggle()
}
}
}
Solved my answer, I needed to add a longer delay for the progress ring circle. Thanks for the commentators in my post that told me about the new Animation() init
struct RingView: View {
var color1 = Color.red
var color2 = Color.purple
var width: CGFloat = 88
var height: CGFloat = 88
var percent: CGFloat = 88
#Binding var show: Bool
#State var progressCircle: Bool = false
var body: some View {
let multiplier = width / 44
let progress = 1 - (percent / 100)
return ZStack {
//Inactive String, the grey circle
Circle()
.stroke(Color.black.opacity(0.1), style: StrokeStyle(lineWidth: 5 * multiplier))
.frame(width: width, height: height)
Circle()
.trim(from: progressCircle ? progress : 1, to: 1)
.stroke(style: StrokeStyle(lineWidth: 5 * multiplier, lineCap: .round))
.rotationEffect(Angle(degrees: 90))
.rotation3DEffect(Angle(degrees: 180), axis: (x: 1, y: 0, z: 0))
.frame(width: width, height: height)
.animation(.linear(duration: 1.0).delay(2.0), value: progressCircle)
Text("\(Int(percent))%")
.font(.system(size: 14 * multiplier))
.fontWeight(.bold)
}
.animation(.linear.delay(0.5), value: show)
.onAppear {
self.progressCircle.toggle()
}
}
}
Related
I'm using GeometryReader() and .frame() to show their comparability and playing with their width.
However the object wrapped in GeometryReader() doesn't have center alignment when width is less than minimum.
Please refer to attached gifs for demo.
Could you please help me with alignment?
Ideal:
Current:
struct ExampleView: View {
#State private var width: CGFloat = 50
var body: some View {
VStack {
SubView()
.frame(width: self.width, height: 120)
.border(Color.blue, width: 2)
Text("Offered Width is \(Int(width))")
Slider(value: $width, in: 0...200, step: 5)
}
}
}
struct SubView: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.yellow.opacity(0.6))
.frame(width: max(geometry.size.width, 120), height: max(geometry.size.height, 120))
}
}
}
That's because GeometryReader doesn't center its children.
You have to manually position the Rectangle by adding either a .position modifier or a .offset.
.position will move the origin of the rectangle's frame relative to the parent's center.
.offset will not change the frame, but rather change the rendering (working like a CGAffineTransform with translation).
The following modifiers to your Rectangle will yield the same results visually (though different under the hood):
Rectangle()
.fill(Color.yellow.opacity(0.6))
.frame(width: max(geometry.size.width, 120), height: max(geometry.size.height, 120))
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
or
let rectWidth = max(geometry.size.width, 120)
Rectangle()
.fill(Color.yellow.opacity(0.6))
.frame(width: rectWidth, height: max(geometry.size.height, 120))
.offset(x: ((geometry.size.width - rectWidth) / 2), y: 0)
Note that your rectangle's frame exceeds the bounds of its parent. I'd suggest to avoid that because it will cause all sorts of difficulties laying out other UI elements on the screen.
You could build it the other way around (unless your practical goal is to just understand how GeometryReader works):
struct ExampleView: View {
#State private var width: CGFloat = 50
var body: some View {
VStack {
let minWidth: CGFloat = 120
let subViewWidth: CGFloat = max(minWidth, width)
SubView(desiredWidth: width)
.frame(width: subViewWidth, height: 120)
.background(.yellow.opacity(0.6))
Text("Offered Width is \(Int(width))")
Slider(value: $width, in: 0...200, step: 5)
}
}
}
struct SubView: View {
let desiredWidth: CGFloat
var body: some View {
Rectangle()
.fill(.clear)
.border(Color.blue, width: 2)
.frame(width: desiredWidth, height: nil)
}
}
In this example the SubView has a yellow fill and a minimal frame width while the inner rectangle just takes whatever width is set on the slider. It also doesn't need a GeometryReader anymore. It looks and behaves the same but none of the views exceeds its parent's bounds anymore.
I am using down code for creating my marching ants border effect, the issue is here that the marching will be not complete correctly if I choose dashWidth bigger than 20.0, as you can see I want my dashWidth be 40.0 but as you see the result is not good. what is the formula to make any dashWidth marchable?
struct ContentView: View {
#State private var dashWidth: CGFloat = 40.0
#State private var toggleClockwise: Bool = Bool()
var body: some View {
Text("dashWidth: " + String(describing: Int(dashWidth))).bold()
RoundedRectangle(cornerRadius: 20.0)
.strokeBorder(style: StrokeStyle(lineWidth: 2.0, lineCap: .round, dash: [dashWidth], dashPhase: toggleClockwise ? -2.0*dashWidth : .zero))
.foregroundColor(Color.blue)
.frame(width: 200, height: 200)
.onAppear() { toggleClockwise.toggle() }
.animation(Animation.linear(duration: 0.5).repeatForever(autoreverses: false), value: toggleClockwise)
}
}
I have a Capsule which change it`s position on screen I want to make the Capsule get squeezed on both ends, but the animation does not allow me to do this, because it takes 2 View as start and end, and I just could manage left and right changing effect, so there is no room for squeezed effect to work! How can I do this as well? like apple done it.
struct CapsuleView: View {
#State private var startAnimation: Bool = Bool()
var body: some View {
return Capsule()
.fill(Color.secondary)
.frame(height: 5, alignment: .center)
.overlay(Capsule().fill(Color.green).frame(width: 100.0, height: 5, alignment: Alignment.center), alignment: startAnimation ? Alignment.trailing : Alignment.leading)
.onAppear() { startAnimation.toggle() }
.animation(Animation.easeInOut(duration: 1.0).repeatForever(autoreverses: true), value: startAnimation)
}
}
Quick and dirty code, but based on #Zaphod`s proposal try using a mask and put your capsule behind it. then animate the offset based on the frame sizes
struct SnakeLoading: View {
#State
private var animating: Bool = true
private let height: CGFloat = 10
private let capsuleWidth: CGFloat = 100
func leadingOffset(size: CGSize) -> CGFloat {
(-size.width - capsuleWidth) * 0.5 + height
}
func trailingOffset(size: CGSize) -> CGFloat {
(size.width + capsuleWidth) * 0.5 - height
}
var body: some View {
GeometryReader { geo in
ZStack {
// Background
Rectangle()
.fill(Color.gray)
// Capsule
Capsule()
.fill(Color.green)
.offset(x: animating ? leadingOffset(size: geo.size) : trailingOffset(size: geo.size))
.frame(width: capsuleWidth, height: height)
.onAppear() { animating.toggle() }
.animation(Animation.easeInOut(duration: 1.0).repeatForever(autoreverses: true), value: animating)
}
.mask(Capsule()
.frame(height: height)
)
}
.padding()
}
}
I've developed a repeating animation that starts out silky smooth but starts to get quite choppy after 10 or 20 seconds doing nothing else on my device. This is the entire code for the view (it is set as the content view of a single view app):
import SwiftUI
struct MovingCirclesView: View {
#State var animationPercent: Double = 0
var body: some View {
ZStack {
MovingCircle(animationPercent: $animationPercent, size: 300, movementRadius: 100, startAngle: 0)
.offset(x: -200, y: -200)
MovingCircle(animationPercent: $animationPercent, size: 400, movementRadius: 120, startAngle: .pi * 3/4)
.offset(x: 50, y: 300)
MovingCircle(animationPercent: $animationPercent, size: 350, movementRadius: 200, startAngle: .pi * 5/4)
.offset(x: 10, y: 30)
MovingCircle(animationPercent: $animationPercent, size: 230, movementRadius: 80, startAngle: .pi * 1/2)
.offset(x: 220, y: -300)
MovingCircle(animationPercent: $animationPercent, size: 230, movementRadius: 150, startAngle: .pi)
.offset(x: 220, y: 100)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.animation(Animation.linear(duration: 30).repeatForever(autoreverses: false))
.onAppear() { self.animationPercent = 1 }
}
private struct MovingCircle: View, Animatable {
#Binding var animationPercent: Double
let size: CGFloat
let movementRadius: CGFloat
let startAngle: Double
var body: some View {
Circle()
.frame(width: size, height: size)
.foregroundColor(Color.white)
.opacity(0.1)
.offset(angle: .radians(.pi * 2 * self.animationPercent + self.startAngle), radius: self.movementRadius)
}
}
}
struct MovingCirclesView_Previews: PreviewProvider {
static var previews: some View {
MovingCirclesView()
.background(Color.black)
.edgesIgnoringSafeArea(.all)
}
}
struct AngularOffset: AnimatableModifier {
var angle: Angle
var radius: CGFloat
var animatableData: AnimatablePair<Double, CGFloat> {
get {
return AnimatablePair(angle.radians, radius)
}
set {
self.angle = .radians(newValue.first)
self.radius = newValue.second
}
}
func body(content: Content) -> some View {
return content.offset(CGSize(
width: CGFloat(cos(self.angle.radians)) * self.radius,
height: CGFloat(sin(self.angle.radians)) * self.radius
))
}
}
extension View {
func offset(angle: Angle, radius: CGFloat) -> some View {
ModifiedContent(content: self, modifier: AngularOffset(angle: angle, radius: radius))
}
}
The general idea is that there are a series of semi-transparent circles slowly moving in circles. I'm concerned this will not be worth the energy usage and was planning to profile anyway, but to my surprise, the animation seems to get bogged down rather quickly. I profiled it on an iPhone X and I don't see any increase in CPU usage nor Memory usage over time as the animation gets more and more choppy.
Does anyone have an idea of why the animation gets choppy? Anything I can do to fix that or do I have to throw out this idea?
Update: Xcode 13.4 / iOS 15.5
As on now it seems drawingGroup does not have such positive effect as was before, because with used .animation(.., value:) animation works properly and with low resource consumption.
Test module on GitHub
Original
Here is a solution - activating Metal by using .drawingGroup and using explicit animation.
Works fine with Xcode 11.4 / iOS 13.4 - tested during 5 mins, CPU load 4-5%
ZStack {
// .. circles here
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.drawingGroup()
.onAppear() {
withAnimation(Animation.linear(duration: 30).repeatForever(autoreverses: false)) {
self.animationPercent = 1
}
}
Note: findings - it looks like implicit animation recreates update stack in this case again and again, so they multiplied, but explicit animation activated only once.
For some raisons, in my app in SwiftUI, I need to use transformEffect modifier with CGAffineTransform and rotationAngle property. But the result is la this:
How can I set the anchor of rotation angle to center of my arrow image? I see in the document that can we use CGPoint?
My code:
import SwiftUI
import CoreLocation
struct Arrow: View {
var locationManager = CLLocationManager()
#ObservedObject var heading: LocationManager = LocationManager()
private var animationArrow: Animation {
Animation
.easeInOut(duration: 0.2)
}
var body: some View {
VStack {
Image("arrow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
.overlay(RoundedRectangle(cornerRadius: 0)
.stroke(Color.white, lineWidth: 2))
.animation(animationArrow)
.transformEffect(
CGAffineTransform(rotationAngle: CGFloat(-heading.heading.degreesToRadians))
.translatedBy(x: 0, y: 0)
)
Text(String(heading.heading.degreesToRadians))
.font(.system(size: 30))
.fontWeight(.light)
}
}
}
If you want to do it with a transform matrix, you'll need to translate, rotate, translate. But you can perform a rotationEffect with an anchor point. Note that the default anchor point is center. I am just including it explicity, so it works if you want to rotate anchoring somewhere else.
The anchor point is specified in UnitPoint, which means coordinates are 0 to 1. With 0.5 meaning center.
struct ContentView: View {
#State private var angle: Double = 0
var body: some View {
VStack {
Rectangle().frame(width: 100, height: 100)
.rotationEffect(Angle(degrees: angle), anchor: UnitPoint(x: 0.5, y: 0.5))
Slider(value: $angle, in: 0...360)
}
}
}
The same effect, using matrices, is uglier, but also works:
struct ContentView: View {
#State private var angle: Double = 0
var body: some View {
VStack {
Rectangle().frame(width: 100, height: 100)
.transformEffect(
CGAffineTransform(translationX: -50, y: -50)
.concatenating(CGAffineTransform(rotationAngle: CGFloat(angle * .pi / 180)))
.concatenating(CGAffineTransform(translationX: 50, y: 50)))
Slider(value: $angle, in: 0...360)
}
}
}