SwiftUI flower petal drawing pitfalls: Shape Protocol practice

Detailed record of the complete process of drawing custom petal shapes using SwiftUI Shape Protocol, including practical tips such as addQuadCurve and coordinate system

The Origin

Recently, I wanted to port my WeChat Mini Program "Petal Word Challenge" to iOS. This mini-program is a game where you tap letters on petals to form words. Drawing petals with CSS in the mini-program was straightforward, but switching to SwiftUI gave me a headache.

As someone with half-baked SwiftUI skills, having only built a few technically simple apps before, I had no idea where to start drawing a petal. After googling "SwiftUI petal," I found this article. While it wasn't exactly what I envisioned, it at least gave me a starting point.

What is the Shape Protocol?

Simply put, SwiftUI's Shape Protocol is a tool for creating custom shapes. You just need to implement a path(in rect: CGRect) -> Path method to define how the shape should be drawn.

First Attempt: Drawing Arcs with addArc

The referenced article used the addArc method:

struct DaisyPental: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.move(to: CGPoint(x: rect.minX, y: rect.midY))
        path.addArc(
            center: CGPoint(x: rect.maxX, y: rect.midY), 
            radius: rect.maxX*(1/11),
            startAngle: .degrees(90),
            endAngle: .degrees(270),
            clockwise: true
        )
        return path
    }
}

To understand what this code did, I tried it in Xcode, adding a red background for clarity:

struct ShapTest: View {
    var body: some View {
        DaisyPental()
            .fill(.yellow.gradient)
            .frame(width: 110, height: 20)
            .background(.red)  // Red background for visibility
    }
}

SwiftUI's Coordinate System is Counterintuitive

Here's a gotcha: SwiftUI's coordinate system doesn't work like you might expect:

  • Origin at Top-Left: Not bottom-left.
  • X-axis: Positive to the right (this is normal).
  • Y-axis: Positive downwards (this is counterintuitive!).

Therefore:

  • rect.minX, rect.minY → Top-left corner
  • rect.maxX, rect.maxY → Bottom-right corner
  • rect.midX, rect.midY → Center point

You can experiment by modifying the x and y values in path.move while watching the Xcode Canvas preview.

addArc Angles are Even More Counterintuitive

The angle definition for addArc is particularly quirky:

  • 0° → Right
  • 90° → Down (Note: Not up!)
  • 180° → Left
  • 270° → Up

This is completely different from the standard mathematical coordinate system and tripped me up for a while.

addQuadCurve: The Curve-Drawing Power Tool

I later realized addArc couldn't create the petal shape I wanted. Seeing someone use addQuadCurve on YouTube, I tried it and found it much more suitable.

The principle of addQuadCurve is simple:

path.addQuadCurve(
    to: CGPoint,      // End point
    control: CGPoint  // Control point
)
  • Start Point: The current position of the drawing pen.
  • End Point: Where you want the curve to end.
  • Control Point: This point determines how the curve bends. Adjusting it changes the curvature effect.

I don't fully grasp the underlying math, but tweaking the control point position visibly alters the curve.

My Final Petal Implementation

After much experimentation, I arrived at this petal shape:

import SwiftUI

struct PetalShape: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            // Start drawing from this point
            path.move(to: CGPoint(x: rect.minX + 30, y: rect.minY + 30))
            
            // Draw left curve of the petal
            path.addQuadCurve(
                to: CGPoint(x: rect.minX + 30, y: rect.minY - 30),
                control: CGPoint(x: rect.minX - 10, y: rect.minY)
            )
            
            // Draw the tip of the petal
            path.addQuadCurve(
                to: CGPoint(x: rect.maxX * 1.8, y: rect.minY),
                control: CGPoint(x: rect.midX + 40, y: -rect.maxY + 30)
            )
            
            // Draw the right side of the petal, returning to start
            path.addQuadCurve(
                to: CGPoint(x: rect.minX + 30, y: rect.minY + 30),
                control: CGPoint(x: rect.midX + 40, y: rect.maxY - 30)
            )
        }
    }
}

Usage example:

struct ShapeTest: View {
    var body: some View {
        PetalShape()
            .fill(.yellow.gradient)
            .frame(width: 110, height: 110)
    }
}

Next Step: Composing the Full Flower

With one petal shape ready, assembling a full flower becomes straightforward:

  1. Duplicate several petals.
  2. Rotate each using rotationEffect.
  3. Adjust positions with offset.
  4. Combine them into a complete flower.

Lessons Learned

Key takeaways from this process:

  • SwiftUI's Coordinate System: Is genuinely counterintuitive, especially the downward Y-axis and angle definitions.
  • addQuadCurve > addArc: Far superior for drawing natural curves.
  • Control Point is Key: Experimenting with its position is crucial for the desired effect.
  • Live Preview is Essential: Use the Canvas preview constantly to see changes immediately.

Finally

Despite the twists and turns, I successfully created the petal shape. I can now continue developing the iOS version of "Petal Word Challenge" and aim for a release soon.

I wrote this post primarily to document the pitfalls encountered. While it might not be perfectly clear, the willingness to document it is progress in itself.

If you're also working on custom SwiftUI shapes, I hope this helps. Feel free to discuss any questions!

References