Repeating SwiftUI animation gets choppy over time - swift

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.

Related

Alignment for GeometryReader and .frame

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.

Animation is animating entire circle and not the progress Xcode 13

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()
}
}
}

SwiftUI transition(.asymmetric..) not working with Offset

I am working on a Set Game and I want the cards to swing on the view (from off the screen) via the .transition(.asymmetric(insertion: .offset(x: 100.0, y: 100.0), removal: .offset(x: 100.0, y: 100.0))) command.
Actually, the cards leave the screen by doing exactly the offset, but they just 'appear' at the start on the program rather than actually coming off the screen..
Video of the App
As you can see the Cards that are selected and 'matched' fly off the screen with the .transition(offset).
I would like to see the cards fly in at the beginning of the Game from off the screen in the same manner.
#ViewBuilder
private func body (for size: CGSize) -> some View{
let paddingAllSidesInCards:CGFloat = 8
if card.isFaceUp || !card.isMatched
{
GeometryReader { geo in
VStack(alignment: .center) {
Group
{
ForEach( 0..<card.numberofsymbols)
{ numberofcards in
let cardshape = cardViewModelShape.getShape(Card: card, size: size, numberOfCards: card.numberofsymbols)
cardshape
.transition(.identity)
.aspectRatio(1,contentMode: .fit)
.frame(minWidth: 20, idealWidth: 100, maxWidth: .infinity, minHeight: 20, idealHeight: 100, maxHeight: .infinity, alignment: .center)
.scaleEffect(card.isSelected ? 1.2 : 1)
.animation(.spring(response: (card.isSelected ? 1:0), dampingFraction: 0.1, blendDuration: .infinity))
}.padding(paddingAllSidesInCards)
//Implicit animation - with linear execution
}
}
.cardify(isFaceUp: card.isFaceUp)
}
.transition(.asymmetric(insertion: .offset(x: 100.0, y: 100.0), removal: .offset(x: 100.0, y: 100.0)))
}
}
I am doing something wrong or forgetting a point - I tried the onAppear() operator and it did not work.
Thanks for your help!
PS: If you want to see the full source code, you can check out the repository:
Link SetGame Repo
Provided code is not testable, so just an idea - try to wrap everything into container (it is needed as a placeholder to animate transitions), like
#ViewBuilder
private func body (for size: CGSize) -> some View{
let paddingAllSidesInCards:CGFloat = 8
VStack // << this one !!
{
if card.isFaceUp || !card.isMatched
{
GeometryReader { geo in
VStack(alignment: .center) {
// .. other code
}
.cardify(isFaceUp: card.isFaceUp)
}
.transition(.asymmetric(insertion: .offset(x: 100.0, y: 100.0), removal: .offset(x: 100.0, y: 100.0)))
}
}
.animation(.default) // << use any you want
}
As always the solution was near by - I have placed my animation and transition at the wrong place --> I have tried to place it for each individual card, but actually, I wanted every card at the same time glide in view.
There are different ways to do it and I decided to use a state and an animated offset, so that the cards glide into the view.
struct SetGameView: View {
#ObservedObject var viewModel: SetGameViewModel
#State private var offset = 1000
//var card: SetGameModel<String>.Card
var body: some View {
VStack()
{
Grid(viewModel.cards) { card in
Cardview(cardViewModelShape: viewModel, card: card).onTapGesture
{
withAnimation(.linear(duration: self.AnimationDuration))
{
self.viewModel.choose(card: card)
}
}
}.offset(x: 0.0, y: CGFloat(offset)) // That's important
}

Rotate view to always face mouse pointer in SwiftUI

Background: I am trying to have a view rotate to face the mouse at all times.
Details of issue: Moving the view based solely on the mouse moving left or right or on up or down works fine (see version 1). But using both at the same time (non-linear or arc motion with the mouse) fails to work as soon as the mouse arcs down (as y goes negative) and around (as x goes negative) and the position of the view regresses.
For this I am attempting to dynamically switch rotation values from negative to positive depending on what side of the screen the mouse is on (see version 2). But this feels like a poor implementation and is quite buggy.
Question: How can I better do this?
I am basing my cursor fetching code off of here.
Version 1-
Problem: moving the view based solely on the mouse moving left or right or on up or down works fine but this issue occurs when trying to move the mouse in a non-linear way:
import SwiftUI
struct ContentView: View {
var window: NSWindow! = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
var mouseLocation: NSPoint { NSEvent.mouseLocation }
var location: NSPoint { window.mouseLocationOutsideOfEventStream }
#State var position: NSPoint = NSPoint.init()
var body: some View {
GeometryReader { geometry in
VStack {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
.rotationEffect(
(self.position.y > 60) ?
.degrees(Double(self.position.x) / 2)
: .degrees(Double(self.position.x) / 2)
)
.rotationEffect(
(self.position.x > 320) ?
.degrees(Double(self.position.y * -1))
: .degrees(Double(self.position.y) / 2)
)
}.frame(width: geometry.size.width, height: geometry.size.height)
.onAppear() {
//Setup
self.window.center();
self.window.setFrameAutosaveName("Main Window")
/* Get mouse location on movement*/
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
self.position = self.location //Save location
print("mouse location:", Double(self.position.x), Double(self.position.y))
return $0
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Version 2- kind of works but is buggy:
import SwiftUI
struct ContentView: View {
var window: NSWindow! = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
var mouseLocation: NSPoint { NSEvent.mouseLocation }
var location: NSPoint { window.mouseLocationOutsideOfEventStream }
#State var position: NSPoint = NSPoint.init()
var body: some View {
GeometryReader { geometry in
VStack {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
.rotationEffect(
(self.position.x > 181) ?
.degrees(Double(self.position.x) / 2)
: .degrees(Double(self.position.x * -1) / 2)
)
.rotationEffect(
(self.position.x > 181) ?
.degrees(Double(self.position.y * -1) / 2)
: .degrees(Double(self.position.y) / 2)
)
}.frame(width: geometry.size.width, height: geometry.size.height)
.onAppear() {
//Setup
self.window.center();
self.window.setFrameAutosaveName("Main Window")
/* Get mouse location on movement*/
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
self.position = self.location //Save location
print("mouse location:", Double(self.position.x), Double(self.position.y))
return $0
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
check this out:
few remarks:
1) i changed it to ios (should be easy to change back to macos because the difference is just the getting of the position
2) i made a gradient to the rectangle so that is clear, in which direction the rectangle rotates
3) i have no idea what you are calculating, but in my opinion you can only do it correctly with trigonometric functions
struct ContentView: View {
#State var position : CGPoint = .zero
#State var offset : CGSize = .zero
#State var translation : CGSize = .zero
#State var angle : Double = 0
func calcAngle(_ geometry: GeometryProxy) -> Angle {
let rectPosition = CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)
// tan alpha = dx / dy
var alpha : Double
if rectPosition.y == position.y {
alpha = 0
} else {
var dx = (position.x - rectPosition.x)
let dy = (position.y - rectPosition.y)
if dy > 0 {
dx = -dx
}
let r = sqrt(abs(dx * dx) + abs(dy * dy))
alpha = Double(asin(dx / r))
if dy > 0 {
alpha = alpha + Double.pi
}
}
return Angle(radians: alpha)
}
var body: some View {
GeometryReader { geometry in
ZStack {
Rectangle()
.fill(LinearGradient(gradient: Gradient(colors: [Color.red, Color.yellow]), startPoint: UnitPoint.top, endPoint: UnitPoint.bottom))
.frame(width: 200, height: 200)
.position(CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2))
.rotationEffect(self.calcAngle(geometry), anchor: UnitPoint(x: 0.5, y: 0.5))
Circle().fill(Color.blue).frame(width:20,height:20).position(self.position)
Circle().fill(Color.green).frame(width:20,height:20).position(self.position)
}.frame(width: geometry.size.width, height: geometry.size.height)
.gesture(
DragGesture()
.onChanged { gesture in
self.position = gesture.location
self.translation = gesture.translation
}
.onEnded { _ in
}
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

How to change anchor of rotation angle to center with CGAffineTransform?

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)
}
}
}