How to Animate Path in SwiftUI
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 }
}
}
animate path stroke drawing in SwiftUI
Re-tested: Xcode 13.4 / iOS 15.5
It can be used .trim
with animatable end, like below with your modified code
struct MyLines: View {
var height: CGFloat
var width: CGFloat
@State private var percentage: CGFloat = .zero
var body: some View {
// ZStack { // as for me, looks better w/o stack which tighten path
Path { path in
path.move(to: CGPoint(x: 0, y: height/2))
path.addLine(to: CGPoint(x: width/2, y: height))
path.addLine(to: CGPoint(x: width, y: 0))
}
.trim(from: 0, to: percentage) // << breaks path by parts, animatable
.stroke(Color.black, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.animation(.easeOut(duration: 2.0), value: percentage) // << animate
.onAppear {
self.percentage = 1.0 // << activates animation for 0 to the end
}
//}
}
}
SwiftUI: path not animating on change of binding animation
We need animatable data in shape and actually do not need binding but animation directly on Arc.
Tested with Xcode 13.4 / watchOS 8.5
Here is main part of fixed code:
struct Arc: View {
var body: some View {
ArcShape(value: value) // << here !!
.stroke(lineWidth: 3)
.animation(.easeOut(duration:2), value: value)
}
// ...
struct ArcShape : Shape {
var animatableData: CGFloat {
get { value }
set { value = newValue }
}
// ...
Complete test module is here
SwiftUI | Animate This Path Shape
Here is possible approach - move path into custom shape and make changed parameter as animatable property.
Update: Re-tested with Xcode 13.3 / iOS 15.4
struct MyArrow: Shape {
var width: CGFloat
var offset: CGFloat
var animatableData: CGFloat {
get { offset }
set { offset = newValue }
}
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: .zero)
path.addLine(to: CGPoint(x: width/2, y: offset))
path.move(to: CGPoint(x: width/2, y: offset))
path.addLine(to: CGPoint(x: width, y: 0))
}
}
}
struct ContentView: View {
@State private var change = false
private let arrowWidth: CGFloat = 80
var body: some View {
MyArrow(width: arrowWidth, offset: change ? -20 : 20)
.stroke(style: StrokeStyle(lineWidth: 12, lineCap: .round))
.frame(width: arrowWidth)
.foregroundColor(.green)
.contentShape(Rectangle())
.onTapGesture { withAnimation { change.toggle() } }
.onTapGesture { change.toggle() }
.padding(.top, 300)
}
}
Swiftui animate each line while drawing a line graph using path
Here is a demo of possible solution. Tested with Xcode 12.
struct TestAnimateAddShape: View {
@State private var end = CGFloat.zero
var body: some View {
GeometryReader { geometry in
LineChartShape(chartItems: [
ChartItem(y: 0.5, x: 0.05),
ChartItem(y: 0.4, x: 0.1),
ChartItem(y: 0.2, x: 0.15),
ChartItem(y: 0.3, x: 0.2),
ChartItem(y: 0.3, x: 0.25),
ChartItem(y: 0.4, x: 0.3),
ChartItem(y: 0.5, x: 0.35),
ChartItem(y: 0.3, x: 0.4),
ChartItem(y: 0.6, x: 0.45),
ChartItem(y: 0.65, x: 0.5),
ChartItem(y: 0.5, x: 0.55),
ChartItem(y: 0.5, x: 0.6),
ChartItem(y: 0.4, x: 0.65),
ChartItem(y: 0.45, x: 0.7),
ChartItem(y: 0.3, x: 0.75),
ChartItem(y: 0.3, x: 0.8),
ChartItem(y: 0.2, x: 0.85),
ChartItem(y: 0.3, x: 0.9)
])
.trim(from: 0, to: end) // << here !!
.stroke(Color.blue, lineWidth: 5)
.animation(.easeInOut(duration: 10))
}.onAppear { self.end = 1 } // << activate !!
}
}
Animation after animation working with Path in SwiftUI
You toggled trim
in wrong way that should be done in to:
import SwiftUI
struct ContentView: View {
@State private var startDraw: Bool = Bool()
var body: some View {
VStack(spacing: 30.0) {
Path { path in
path.addArc(center: CGPoint(x: 100, y: 100), radius: 50.0, startAngle: Angle(degrees: 0.0), endAngle: Angle(degrees: 360.0), clockwise: true)
path.addLine(to: CGPoint(x: 200, y: 100))
}
.trim(from: 0.0, to: startDraw ? 1.0 : 0.0)
.stroke(style: StrokeStyle(lineWidth: 10.0, lineCap: .round, lineJoin: .round))
.shadow(color: Color.black.opacity(0.2), radius: 0.0, x: 20, y: 20)
.frame(width: 250, height: 200, alignment: .center)
.background(Color.yellow)
.foregroundColor(Color.purple)
.cornerRadius(10.0)
.animation(Animation.easeOut(duration: 3), value: startDraw)
Button("start") { startDraw.toggle() }.font(Font.body.bold())
}
.shadow(radius: 10.0)
}
}
Related Topics
In Swift, Does Int Have a Hidden Initializer That Takes a String
Enumerate Is Unavailable Call the Enumerate Method on the Sequence
How to Convert Hexstring to Bytearray in Swift 3
Nstimer.Scheduledtimerwithtimeinterval in Swift Playground
How to Subclass Nsoperation in Swift to Queue Skaction Objects for Serial Execution
Uibarbuttonitem Selector Not Working
Making Swift Generics Play with Overloaded Functions
How Do Generators Whose Element Is Optional Know When They'Ve Reached the End
How to Say "If X == a or B or C" as Succinctly in Swift as Possible
Swift - Reorder Uitableview Cells
Skaction Completion Handlers; Usage in Swift
Swift, Sprite Kit Game: Have Circle Disappear in Clockwise Manner? on Timer
Swift & Firebase - How to Store More User Data Other Than Email and Password
Is There a Daylight Savings Check in Swift