How to Animate Path in Swiftui

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.

Sample Image

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

Sample Image

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

demo

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

demo

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.

demo

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:


Sample Image



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



Leave a reply



Submit