What You Will Build
A three-stage drawing animation: two path segments draw a checkmark stroke-by-stroke, then a circle draws around it. This is the confirmation animation used after successful payments, form submissions, or onboarding completions.
Why This Pattern Matters
Apple Pay, Stripe, and most banking apps use checkmark animations after transactions. The .trim(from:to:) technique is one of the most reusable skills in SwiftUI — it applies to progress indicators, signature animations, and any custom shape reveal.
Step 1: Three Animation States
@State private var drawPercentage1: CGFloat = 0 // first check stroke
@State private var drawPercentage2: CGFloat = 0 // second check stroke
@State private var drawCircle: CGFloat = 0 // surrounding circle
Step 2: Draw the Checkmark Path
// First stroke: upper-left to bottom-center
Path { path in
path.move(to: CGPoint(x: 70 * scale, y: 60 * scale))
path.addLine(to: CGPoint(x: 108 * scale, y: 100 * scale))
}
.trim(from: 0, to: drawPercentage1)
.stroke(style: StrokeStyle(lineWidth: 8, lineCap: .round, lineJoin: .round))
.foregroundStyle(.green)
// Second stroke: bottom-center to upper-right
Path { path in
path.move(to: CGPoint(x: 108 * scale, y: 100 * scale))
path.addLine(to: CGPoint(x: 168 * scale, y: 40 * scale))
}
.trim(from: 0, to: drawPercentage2)
.stroke(style: StrokeStyle(lineWidth: 8, lineCap: .round, lineJoin: .round))
.foregroundStyle(.green)
Step 3: Trigger the Sequence
func startAnimation() {
withAnimation(.easeInOut(duration: 1)) { drawPercentage1 = 1.0 }
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
withAnimation(.easeInOut(duration: 1)) { drawPercentage2 = 1.0 }
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation(.easeInOut(duration: 1)) { drawCircle = 0.93 }
}
}
Tips and Pitfalls
- Why 0.93: A fully closed circle looks mechanical. The small gap gives it a hand-drawn quality.
- .lineCap(.round) is critical for smooth stroke endpoints.
- Reset before replay: Set all three values to 0 before re-triggering.
- Add haptics: Call
UIImpactFeedbackGenerator(style: .medium).impactOccurred()at completion.
iOS Version: iOS 15+