Creating Shapes in SwiftUI | SwiftUI Bootcamp #3
Shapes are the building blocks of custom UI chrome — buttons, cards, avatars, progress indicators, and decorative backgrounds all rely on them. After this lesson you'll be able to fill, stroke, and trim any of SwiftUI's built-in shapes, and understand when to use stroke vs strokeBorder.
What You'll Learn
- The five built-in SwiftUI shapes:
Circle,Ellipse,Capsule,Rectangle,RoundedRectangle - The difference between
.stroke(),.strokeBorder(), and.fill() - How to create dashed borders and partial shapes using
StrokeStyleand.trim()
Mental Model
Think of SwiftUI shapes like cookie cutters. The shape defines the outline of the cookie, but the cutter itself doesn't have color or thickness — you apply those separately. .fill() is like filling the cookie with frosting (color inside the outline). .stroke() is like tracing the outline with a marker — but the marker straddles the edge, so half of it goes outside your shape and half goes inside. .strokeBorder() is the polite version: it keeps the entire stroke inside the shape boundary, so it never bleeds beyond the frame you set.
This "inside the edge" distinction is the most common source of confusion for beginners, and understanding it early saves a lot of debugging.
Detailed Explanation
SwiftUI provides five built-in shapes, all conforming to the Shape protocol. Circle and Ellipse are round; Rectangle has sharp corners; Capsule is a rectangle with fully rounded ends; RoundedRectangle(cornerRadius:) lets you control the rounding amount.
Shapes are fill-only by default — they fill their available space with the foreground color. To change the fill color, use .fill(Color.green) or .foregroundColor(.pink). These are equivalent but .fill() is preferred because it signals intent clearly and accepts any ShapeStyle (including gradients).
.stroke() paints a border centered on the shape's edge, meaning half the line width extends outside the frame. This can cause the stroke to be clipped if the shape sits at the edge of its container. .strokeBorder() fixes this by drawing the border entirely inside the shape's bounds — it's almost always the right choice for bordered cards and buttons.
StrokeStyle unlocks advanced stroke options: lineWidth, lineCap (how line ends look — .butt, .round, .square), and dash (an array of on/off lengths for dashed lines). .trim(from:to:) draws only a fraction of the shape's path, making it perfect for progress rings.
Code Structure
The sample is in ShapesBootcamp.swift and shows a RoundedRectangle with strokeBorder as the active state. The commented-out lines demonstrate the full range of shape types and stroke variations. Uncomment them one at a time to see how each one looks in the preview canvas.
Complete Code
ShapesBootcamp.swift
import SwiftUI
struct ShapesBootcamp: View {
var body: some View {
//Circle() // perfect circle, fills available space proportionally
//Ellipse() // oval, stretches to fill available width and height separately
//Capsule(style: .circular) // rectangle with fully rounded ends (pill shape)
//Rectangle() // sharp-cornered rectangle
RoundedRectangle(cornerRadius: 10) // rectangle with 10pt rounded corners
//.fill(Color.green) // fills the interior with solid green
//.foregroundColor(.pink) // equivalent to fill — sets the shape's color
//.stroke() // draws a 1pt border centered on the shape edge
//.stroke(Color.red) // same but with an explicit red color
//.stroke(Color.blue, lineWidth: 30) // thick 30pt blue stroke (half outside frame)
//.stroke(Color.orange, style: StrokeStyle(lineWidth: 30, lineCap: .round, dash: [30])) // dashed rounded stroke
//.trim(from: 0.4, to: 1.0) // draws only 60% of the shape's path
// .stroke(Color.purple, lineWidth: 50)
.strokeBorder(Color.red) // draws a 1pt red border entirely inside the shape boundary
.frame(width: 300, height: 200) // fixes the shape's size to 300×200 points
}
}
struct ShapesBootcamp_Previews: PreviewProvider {
static var previews: some View {
ShapesBootcamp()
}
}Code Walkthrough
- Commented shape alternatives — The four commented shapes at the top are direct substitutes for
RoundedRectangle. Swap them in to see how each fills the same300×200frame.Capsulealways produces fully rounded ends regardless of frame size. RoundedRectangle(cornerRadius: 10)— Creates a rounded rect with 10-point corner radii. This is the most common shape for cards, buttons, and input fields in iOS apps..fill()vs.foregroundColor()— Both set the interior color, but.fill()accepts anyShapeStyle(includingLinearGradient,AngularGradient, etc.), while.foregroundColor()only acceptsColor. Prefer.fill()for flexibility..stroke()vs.strokeBorder()— The key difference:.stroke()centers the line on the shape's path (so a 30pt stroke extends 15pt outside the frame), while.strokeBorder()keeps the entire line inside. Use.strokeBorder()for card borders and buttons.StrokeStyle(lineWidth:lineCap:dash:)— Thedasharray alternates between drawn and undrawn segments.[30]means 30pt drawn, 30pt gap, repeating.[10, 5]would mean 10pt drawn, 5pt gap..trim(from: 0.4, to: 1.0)— Draws the shape from the 40% mark to the 100% mark along its path. Combined with.stroke()this makes progress arcs..frame(width: 300, height: 200)— Gives the shape a fixed size. Without a frame, shapes expand to fill all available space in their container.
Common Mistakes
Mistake: Using .stroke() and seeing the border cut off at the container edge This happens because .stroke() draws half the line width outside the shape's bounds, and if the shape fills its parent, that outer half gets clipped. Switch to .strokeBorder() to keep the border fully inside the shape — or add .padding(lineWidth / 2) before .stroke() to give the overflow room.
Mistake: Calling .strokeBorder() on a shape that doesn't support InsettableShapestrokeBorder() is only available on types conforming to InsettableShape. All five built-in shapes support it, but custom Shape implementations may not unless you also conform to InsettableShape.
Mistake: Applying .trim() without also applying .stroke().trim() trims the shape's path, but if the shape is filled, it just draws a pie-slice of fill color. You almost always want .trim() combined with .stroke() or .strokeBorder() to create arc and progress-ring effects.
Key Takeaways
- Use
.strokeBorder()instead of.stroke()whenever the border needs to stay within the shape's frame .fill()is preferred over.foregroundColor()on shapes because it accepts gradients and otherShapeStyletypes.trim(from:to:)combined with.stroke()is the standard technique for progress rings and arc indicators
Last updated: June 27, 2026