drewag
drewag

Reputation: 94723

Repeating SwiftUI animation gets choppy over time

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?

Upvotes: 3

Views: 2626

Answers (1)

Asperi
Asperi

Reputation: 257709

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.

demo

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.

Upvotes: 3

Related Questions