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 - 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
// 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.
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.
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
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))
}
}
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)
}
}
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
Custom Mkannotation Not Moving When Coordinate Set
Closure:Use Unresolved Identifier 'Self'
How to Get Part of a String in Swift
Swift 3: Converting Data to String Returns a Nil Value
Ambiguous Use of Operator '-' in Swift with 'Abs()'
How to Set the iOS13 Uisegmentedcontrol Backgroundcolor to White
Swift Firestore Check If Documents Exists
Using @Discardableresult for Closures in Swift
Left Aligned Horizontal Stackview and Top Aligned Vertical Stackview
Swift - Getting Only Alphanumeric Characters from String
Swift Language Statically or Dynamically Dispatched
Generic Method Override Not Working in Swift
Send Mail with File Attachment
How to Sort Dates in a Dictionary
Textfield in Swiftui Loses Focus When I Enter a Character
How to Use Nsvisualeffectview to Blend Window with Background