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+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.