Drawing in SwiftUI – A Comprehensive Guide

Share on facebook
Share on twitter
Share on pinterest

Hello and welcome to a new tutorial! Today’s tutorial is all about drawing in SwiftUI. We’ll learn how to draw custom graphics, vectors and forms by using the SwiftUI graphics API. First, we are going to take a look at SwiftUI’s built-in shapes and how we can modify them. Then we’re going to compose our own shapes by using custom paths. By the end of this step-by-step tutorial, you’ll be able to:

  • Use and modify prebuilt shapes like Rectangles, Circles etc. for your SwiftUI app
  • Draw your own, custom shapes by using SwiftUI paths
  • Design complex shapes, for example to draw your app icon or use custom badges for your SwiftUI app

Here are some of the designs we’re going to achieve:

Basic Shapes 🔵⬛️

SwiftUI provides us with some basic shapes we can use for drawing in our app. These are:

  • Rectangle
  • RoundedRectangle
  • Ellipse
  • Circle
  • Capsule 

You can use them by simply initialising them within your SwiftUI view. 

struct ShapeSandbox: View {
    var body: some View {
        RoundedRectangle(cornerRadius: 20)
    }
}

By default however, the Shape doesn’t know anything about its supposed size and position. This is why as long as we provide the shape with a .frame it tries to fill out the whole view.

So let’s provide our RoundedRectangle with a certain width and height. We also define another fill color than the default black. 

RoundedRectangle(cornerRadius: 20)
            .frame(width: 250, height: 100)
            .foregroundColor(.purple)

Maybe you want your shape not to be filled out with a certain but rather want to create some kind of border out of it. You can do this by using the .stroke modifier (as the first one!) with choosing a certain lineWidth for your border.

RoundedRectangle(cornerRadius: 20)
            .stroke(lineWidth: 10)
            .frame(width: 250, height: 100)
            .foregroundColor(.purple)

It’s even possible to use another stroke style, for example to use a dashed-line as your border. For example like this:

RoundedRectangle(cornerRadius: 20)
            .stroke(style: StrokeStyle(lineWidth: 7, lineCap: .square, dash: [15], dashPhase: 2))
            .frame(width: 250, height: 100)
            .foregroundColor(.purple)

Of course, you can use the discussed “techniques” for all other shapes!

Drawing own shapes by using paths 🖌

Let’s take a quick look at what paths in SwiftUI are. In a nutshell, you can imagine a path like a set of drawing instructions, including lines, curves and other segments like arcs. This why a shape is doing nothing different than using a specific path to define its appereance. 

Okay, let’s use this knowledge to draw a square by using a Path. To do this, create a new SwiftUI view and call it, for intance, PathSandbox.swift.

Let’s insert a Path instance followed by its corresponding closure. Inside the closure we can actually define how our Path should go. By default again, SwiftUI will fill out resulting view with a black color. While drawing the Path however, I personally prefer to only see the outer border for having a better overview of the resulting Path. This is why I’m applying the .stroke() we got familiar with before starting drawing the Shape.

struct PathSandbox: View {
    var body: some View {
        Path { path in

        }
            .stroke()
    }
}

Let’s draw our square by adding several lines to our Path. We can do this by using absolute x- and y-coordinates. Before drawing the first line, we move the “cursor” right upper corner of our imaginary square. Then we’re adding a line which points to the lower right corner and another line pointing to the lower left corner.

Path { path in
            path.move(to: CGPoint(x: 200, y: 0))
            path.addLine(to: CGPoint(x: 200, y: 200))
            path.addLine(to: CGPoint(x: 0, y: 200))
        }
            .stroke()

You see that two lines got added to our SwiftUI! Let’s finish our square by adding a third line pointing to the upper left corner. We could close the rectangle by adding a last line pointing to where we started, but we can also closing the Path “automatically” by using the .closeSubPath modifier.

Path { path in
            path.move(to: CGPoint(x: 200, y: 0))
            path.addLine(to: CGPoint(x: 200, y: 200))
            path.addLine(to: CGPoint(x: 0, y: 200))
            path.addLine(to: CGPoint(x: 0, y: 0))
            path.closeSubpath()
        }
            .stroke()

Now that we are done with defining our square path, we can fill it out by deleting the .stroke modifier again!

var body: some View {
        Path { path in
            //...
        }
    }

Which results in the following view:

Since, as you already know, a Shape also consists of a Path. For reusability purposes, we can simply convert our Square Path to such a Shape by declaring a struct which adopts the Shape protocol. 

struct MySquare: Shape {
    
}

The single requirement for the Shape protocol is having a path function which draws the actual shape. We can simply use the Path we just created like this:

struct MySquare: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()

        path.move(to: CGPoint(x: 200, y: 0))
        path.addLine(to: CGPoint(x: 200, y: 200))
        path.addLine(to: CGPoint(x: 0, y: 200))
        path.addLine(to: CGPoint(x: 0, y: 0))
        path.closeSubpath()

        return path
    }
}

We now use our MySquare Shape inside our SwiftUI view!

struct PathSandbox: View {
    var body: some View {
        MySquare()
    }
}

However, our MySquare Shape still uses the absolute coordinates we defined earlier. Instead, we want to make it dynamic so that we can transform it by adding a .frame modifier to the MySquare instance inside our SwiftUI view.

We can achieve the rect parameter of our MySquare’s path function. The rect is like a invisible scratchpad inside which we can draw our square and which can gets transformed by passing a certain frame to it when initialising the actual shape.

So let’s exchange the fixed coordinates by the corresponding points of the invisible rect.

struct MySquare: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()

        path.move(to: CGPoint(x: rect.size.width, y: 0))
        path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.width))
        path.addLine(to: CGPoint(x: 0, y: rect.size.width))
        path.addLine(to: CGPoint(x: 0, y: 0))
        path.closeSubpath()

        return path
    }
}

Let’s talk through it very quick. We just move our cursor to the very upper right edge of our invisible rectangle. Then we tell our path to draw a line to another point by using the rect’s width for both (remember, we want to ensure it’s a square) x- and y-coordinates. Then we’re going back to the lower left corner, followed by the upper left corner before closing the subpath. Awesome, now our MySquare Shape is dynamic and we can adjust its size by using the .frame modifier!

struct PathSandbox: View {
    var body: some View {
        MySquare()
            .frame(width: 250, height: 250)
    }
}

Drawing more complex SwiftUI shapes  👨‍🎨

Okay, now that we know how to create our own shapes we practice our SwiftUI drawing skills by creating more complex shapes. Eventually, we’ll rebuild the logo of our website.

To do this, create a new SwiftUI view called BBLogo. The icon should simply consist of a black background with two B letters on it. We start by inserting a ZStack into or BBLogo view so that all views inside it get stacked on top of each other. For the background we use the rounded rectangle shape we got familiar with earlier.

struct BBLogo: View {
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 20)
                .frame(width: 200, height: 200)
        }
    }
}

Next, we draw the B-Letter Shape. Let’s declare a struct conforming to the Shape protocol.

struct LetterB: Shape {
    
    func path(in rect: CGRect) -> Path {
        Path { path in
            
        }
    }
}

To instantly see what we’re drawing we already initialize a LetterB Shape inside our BBLogo view and apply it with a stroke, white color and a certain frame.

ZStack {
            RoundedRectangle(cornerRadius: 20)
                .frame(width: 200, height: 200)
            LetterB()
                .stroke(lineWidth: 12)
                .foregroundColor(.white)
                .frame(width: 100, height: 100)
        }

Let’s start drawing the LetterB Shape! First, we move the “cursor” to the middle of the rect’s upper edge. Then we add a line pointing to the upper left edge of the invisible rectangle. Next, we draw a line to middle of the rect’s left edge followed by a line pointing to center of the rect.

Path { path in
            path.move(to: CGPoint(x: rect.size.width/2, y: 0))
            path.addLine(to: CGPoint(x: 0, y: 0))
            path.addLine(to: CGPoint(x: 0, y: rect.size.width/2))
            path.addLine(to: CGPoint(x: rect.size.width/2, y: rect.size.width/2))
        }

Your preview should look like this now:

Before drawing on, we move our “cursor” back to the middle of the rect’s left edge. Then we draw a line to the lower left edge. Our last line points the middle of the rect’s lower edge.

Path { path in
            path.move(to: CGPoint(x: rect.size.width/2, y: 0))
            path.addLine(to: CGPoint(x: 0, y: 0))
            path.addLine(to: CGPoint(x: 0, y: rect.size.width/2))
            path.addLine(to: CGPoint(x: rect.size.width/2, y: rect.size.width/2))
            path.move(to: CGPoint(x: 0, y: rect.size.width/2))
            path.addLine(to: CGPoint(x: 0, y: rect.size.width))
            path.addLine(to: CGPoint(x: rect.size.width/2, y: rect.size.width))
        }

To complete the letter B we have to add two halved circles to our Shape. We do this by adding two arcs to our Path. When adding arcs, we need to specify the arcs center, as well as the start and end angle. Therefore we write:

Path { path in
            //...
            path.addArc(center: CGPoint(x: rect.size.width/2, y: rect.size.width*(3/4)), radius: rect.size.width/4, startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true)
            path.addArc(center: CGPoint(x: rect.size.width/2, y: rect.size.width/4), radius: rect.size.width/4, startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true)
        }

Awesome, we are finished with creating our LetterB Shape!

We complete the BBLogo by stacking another BLetter Shape on top of the background and the existing one and offsetting it a little bit.

ZStack {
            RoundedRectangle(cornerRadius: 20)
                .frame(width: 200, height: 200)
            LetterB()
                .stroke(lineWidth: 12)
                .foregroundColor(.white)
                .frame(width: 100, height: 100)
            LetterB()
                .stroke(lineWidth: 12)
                .foregroundColor(.white)
                .frame(width: 100, height: 100)
                .offset(x: 40)
        }


That’s it! By building our website’s logo we learned how to create more complex shapes.

Drawing curved shapes in SwiftUI ⤴️🎨

Last but not least, we’ll take a look at how to create curved shapes. By doing this, we’ll be able to create cool shapes like this raindrop icon!

Let’s create new SwiftUI view. Below this, declare a struct called Raindrop and conforming to the Shape protocol.

struct Raindrop: Shape {
    
    func path(in rect: CGRect) -> Path {
        Path { path in

        }
    }
}

Again we start with initialising the Raindrop Shape inside our SwiftUI view and applying a .stroke and .frame to it.

var body: some View {
        Raindrop()
            .stroke(lineWidth: 4)
            .frame(width: 200, height: 200)
    }

Inside our Raindrop’s path we start moving our “cursor” to the middle of the rect’s upper edge. 

Path { path in
            path.move(to: CGPoint(x: rect.size.width/2, y: 0))
        }

Next we want to draw a right curve downwards. To do this, we use the addQuadCurve method. This function adds a so-called Bézier curve to the path. For such a Bézier curve we first need to define an ending point for the curve but to actually do the curve we need to provide the Bézier curve with a control point. Such a control point is used to calculate the strength and direction of the curve. You don’t need to know the calculus for this (nor do i). Just take a look at the following infographic to get a feeling for this.

You see that the direction and strength of the curve depends on where we place the corresponding control point. The further away we place the control point, the more the curve is bent.

Let’s us this knowledge for our addQuadCurve function. We tell the path that the curve should end at the middle of the rect’s lower edge and we place the control point at the lower right edge of the rect

Path { path in
            path.move(to: CGPoint(x: rect.size.width/2, y: 0))
            path.addQuadCurve(to: CGPoint(x: rect.size.width/2, y: rect.size.height), control: CGPoint(x: rect.size.width, y: rect.size.height))
        }

Lets finish our Raindrop Shape by drawing another curve pointing to where we started drawing.

Path { path in
            path.move(to: CGPoint(x: rect.size.width/2, y: 0))
            path.addQuadCurve(to: CGPoint(x: rect.size.width/2, y: rect.size.height), control: CGPoint(x: rect.size.width, y: rect.size.height))
            path.addQuadCurve(to: CGPoint(x: rect.size.width/2, y: 0), control: CGPoint(x: 0, y: rect.size.height))
        }

Awesome, we’re done with our Raindrop shape! Now we can fill out this Shape inside our SwiftUI view by exchanging the .stroke modifier. For example, you can fill the shape with a gradient like this:

Raindrop()
            .fill(LinearGradient(gradient: Gradient(colors: [.white, .blue]), startPoint: .topLeading, endPoint: .bottom))
            .frame(width: 200, height: 200)

Now you’re Raindrop Shape should look like this:

Conclusion 🎊

That’s it! You learned how to use prebuilt shapes and how to create your own by working with Paths. You learned how to use dynamic values instead of fixed coordinates and how to add arcs and curves to your Path. With this knowledge you should be able to create your own drawings and use them inside your SwiftUI app!

I hope you enjoyed this tutorial! If you want to learn more about SwiftUI, make sure you check out our free SwiftUI Basics eBook and our other tutorials! Also make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials and tips about SwiftUI and more!

3 replies on “Drawing in SwiftUI – A Comprehensive Guide”

Hello there! Can you tell me which browser and device you are using? Do you have a ad blocker turned on?

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount

small_c_popup.png

Are you ready for a new era of iOS development?

Start learning swiftUI today - download our free e-book