Animate Path Stroke Drawing in Swiftui

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 - Animate path shape stroke drawing

Update: Xcode 13.4 / iOS 15.5

The .animation has deprecated, so corrected part is

Route(points: self.vm.points, head: self.vm.lastPoint)
.stroke(style: StrokeStyle(lineWidth: 8, lineCap: .round, lineJoin: .miter, miterLimit: 0, dash: [], dashPhase: 0))
.foregroundColor(.red)
.animation(.linear(duration: 0.5), value: self.vm.lastPoint) // << here !!

Test module is here

Original

Here is a demo of possible solution. Tested with Xcode 11.4 / iOS 13.4

demo

// Route shape animating head point
struct Route: Shape {
var points: [CGPoint]
var head: CGPoint

// make route animatable head position only
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get { AnimatablePair(head.x, head.y) }
set {
head.x = newValue.first
head.y = newValue.second
}
}

func path(in rect: CGRect) -> Path {
Path { path in
guard points.count > 1 else { return }
path.move(to: points.first!)
_ = points.dropFirst().dropLast().map { path.addLine(to: $0) }
path.addLine(to: head)
}
}
}

// Route view model holding all points and notifying when last one changed
class RouteVM: ObservableObject {
var points = [CGPoint.zero] {
didSet {
self.lastPoint = points.last ?? CGPoint.zero
}
}
@Published var lastPoint = CGPoint.zero
}

struct DemoRouteView: View {
@ObservedObject var vm = RouteVM()

var body: some View {
GeometryReader { gp in
ZStack { // area
Rectangle().fill(Color.white)
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
self.vm.points.append(value.location) // read coordinates in area
})

Circle().fill(Color.blue).frame(width: 20)
.position(self.vm.points.first!) // show initial point

// draw route when head changed, animating
Route(points: self.vm.points, head: self.vm.lastPoint)
.stroke(style: StrokeStyle(lineWidth: 8, lineCap: .round, lineJoin: .miter, miterLimit: 0, dash: [], dashPhase: 0))
.foregroundColor(.red)
.animation(.linear(duration: 0.5))
}
.onAppear {
let area = gp.frame(in: .global)
// just initail point at the bottom of screen
self.vm.points = [CGPoint(x: area.midX, y: area.maxY)]
}
}.edgesIgnoringSafeArea(.all)
}
}

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 !!
}
}

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 }
}
}

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

How we can draw a stroke for a custom path in SwiftUI?

You'll need to stroke and fill the path separately. Here is one way to do it.

Assign your path to a variable, and then use that to fill it, and then overlay it with the same path stroked. Note: You need to use path.closeSubpath() to close your path, or only the arc will be stroked.

struct ContentView: View
{
var body: some View {
let path = Path { path in
path.addArc(center: CGPoint(x: 100, y: 300), radius: 200, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false)
path.closeSubpath()
}
path.fill(Color.red).overlay(path.stroke(Color.black, lineWidth: 2))
}
}

Sample Image

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)
}
}

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