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:
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?
SwiftUI has built-in support for animations with its animation() modifier. To use this modifier, place it after any other modifiers for your views, tell it what kind of animation you want, and also make sure you attach it to a particular value so the animation triggers only when that specific value changes.
If you want a SwiftUI view to start animating as soon as it appears, you should use the onAppear() modifier to attach an animation.
Click on Live Preview in the Canvas , and then Click on Tap to Animate . Notice that all the Circle moves down and then up, but there's no animation. Let's add animation to it. Again, Click on Live Preview , and then tap on Tap to Animate .
To make our animation repeat, we use . repeatForever() . The default behavior of repeat forever is autoreverse, that's mean our animation will scale from 1 to 0.5 then 0.5 back to 1, creating a seamless loop of animation.
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.
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 }
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With