How to Animate Path in SwiftUI
Asked Answered
R

2

8

Being unfamiliar with SwiftUI and the fact that there is not much documentation on this new framework yet. I was wondering if anyone was familiar with how it would be possible to animate a Path in SwiftUI.

For example given a view, lets say this simple RingView:

struct RingView : View {   
    var body: some View {
        GeometryReader { geometry in
            Group {
                // create outer ring path
                Path { path in
                    path.addArc(center: center,
                                radius: outerRadius,
                                startAngle: Angle(degrees: 0),
                                endAngle: Angle(degrees: 360),
                                clockwise: true)
                }
                .stroke(Color.blue)

                // create inner ring
                Path { path in
                    path.addArc(center: center,
                                radius: outerRadius,
                                startAngle: Angle(degrees: 0),
                                endAngle: Angle(degrees: 180),
                                clockwise: true)
                }
                .stroke(Color.red)
                .animation(.basic(duration: 2, curve: .linear))
            }
        }
        .aspectRatio(1, contentMode: .fit)
    }
}

What is displayed is:

enter image description here

Now I was wondering how we could go about animating the inner ring, that is the red line inside the blue line. The animation I'm looking to do would be a simple animation where the path appears from the start and traverses to the end.

This is rather simple using CoreGraphics and the old UIKit framework but it doesn't seem like adding a simple .animation(.basic(duration: 2, curve: .linear)) to the inner path and displaying the view with a withAnimation block does anything.

I've looked at the provided Apple tutorials on SwiftUI but they really only cover move/scale animations on more in-depth views such as Image.

Any guidance on how to animate a Path or Shape in SwiftUI?

Roughdry answered 4/7, 2019 at 1:47 Comment(0)
U
30

Animation of paths is showcased in the WWDC session 237 (Building Custom Views with SwiftUI). The key is using AnimatableData. You can jump ahead to 31:23, but I recommend you start at least at minute 27:47.

You will also need to download the sample code, because conveniently, the interesting bits are not shown (nor explained) in the presentation: https://developer.apple.com/documentation/swiftui/drawing_and_animation/building_custom_views_in_swiftui


More documentation: Since I originally posted the answer, I continued to investigate how to animate Paths and posted an article with an extensive explanation of the Animatable protocol and how to use it with Paths: https://swiftui-lab.com/swiftui-animations-part1/


Update:

I have been working with shape path animations. Here's a GIF.

enter image description here

And here's the code:

IMPORTANT: The code does not animate on Xcode Live Previews. It needs to run either on the simulator or on a real device.

import SwiftUI

struct ContentView : View {
    var body: some View {
        RingSpinner().padding(20)
    }
}

struct RingSpinner : View {
    @State var pct: Double = 0.0

    var animation: Animation {
        Animation.basic(duration: 1.5).repeatForever(autoreverses: false)
    }

    var body: some View {

        GeometryReader { geometry in
            ZStack {
                Path { path in

                    path.addArc(center: CGPoint(x: geometry.size.width/2, y: geometry.size.width/2),
                                radius: geometry.size.width/2,
                                startAngle: Angle(degrees: 0),
                                endAngle: Angle(degrees: 360),
                                clockwise: true)
                }
                .stroke(Color.green, lineWidth: 40)

                InnerRing(pct: self.pct).stroke(Color.yellow, lineWidth: 20)
            }
        }
        .aspectRatio(1, contentMode: .fit)
            .padding(20)
            .onAppear() {
                withAnimation(self.animation) {
                    self.pct = 1.0
                }
        }
    }

}

struct InnerRing : Shape {
    var lagAmmount = 0.35
    var pct: Double

    func path(in rect: CGRect) -> Path {

        let end = pct * 360
        var start: Double

        if pct > (1 - lagAmmount) {
            start = 360 * (2 * pct - 1.0)
        } else if pct > lagAmmount {
            start = 360 * (pct - lagAmmount)
        } else {
            start = 0
        }

        var p = Path()

        p.addArc(center: CGPoint(x: rect.size.width/2, y: rect.size.width/2),
                 radius: rect.size.width/2,
                 startAngle: Angle(degrees: start),
                 endAngle: Angle(degrees: end),
                 clockwise: false)

        return p
    }

    var animatableData: Double {
        get { return pct }
        set { pct = newValue }
    }
}
Urian answered 4/7, 2019 at 9:41 Comment(11)
And also make sure you stick until the end, where they explain how setting a drawingGroup improves performance dramatically by using Metal.Urian
I updated my answer to include a working example of the ring animation.Urian
I made yet another update. No longer need to use DispatchQueue to create multiple animations. It now just uses one.Urian
I copied the code to a new file in my project and I can see the green circle but no animation (unlike in the GIF you posted). Is there anything I'm missing? Thanks!Outclass
I just double checked. I created a new project, and replaced the default ContentView.swift with my post and works fine. However, I see that Xcode Live Preview does not work. Are you trying the simulator, or a real device. I tried in both and it works.Urian
It indeed works when running on the simulator. I was hoping to see it in the live preview along with some other animations I have. Thanks.Outclass
I stopped using Live Previews on day 2 of my SwiftUI jouney. I could not trust it and ended wasting time instead. I guess I'll wait until the GM.Urian
@Urian wow thanks a lot for the detailed post, I watched that video but must’ve checked out when they mentioned animatable data.Roughdry
You're welcome. To be honest, the demo just mentioned it as if it was nothing. But it took me a while to figure it out. Cheers.Urian
//Type 'Animation' has no member 'basic' Animation.basic(duration: 1.5).repeatForever(autoreverses: false) // You can replace by this Animation.easeIn(duration: 1.5).repeatForever(autoreverses: false)Happily
I get an error "Cannot find type "Animation" in scope. Was that removed from SwiftUI?Hatten
F
0

You can achieve the same using simpler Shape and View APIs. The only important point is to:

  1. Define a @State for what you are trying to animate
  2. Trigger the animation when needed

Here I made a simple rotation animation on a trimmed circle:

struct LoadingSpinner: View {
    @State private var degree = 0.0

    var body: some View {
        Circle()
            .stroke(lineWidth: 40)
            .overlay {
                Circle()
                    .trim(from: 0, to: 0.3)
                    .stroke(style: .init(lineWidth: 20, lineCap: .round))
                    .foregroundStyle(.blue.gradient)
                    .rotationEffect(.degrees(degree)) // 👈 Animating this
            }
            .onAppear {
                withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
                    degree = 360 // 👈 Trigger the change in a "withAnimation" block
                }
            }
    }
}

Demo

Demo

You can find so many custom spinners in this answer if you wish

Fermentative answered 27/6 at 10:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.