Drawing in SwiftUI

How to draw in SwiftUI 🖌

It’s time to learn drawing in SwiftUI! With the following knowledge, you will be able to design your own custom graphics, vectors, and forms. Before we start, let’s create a new SwiftUI view file, name it “Chart.swift” and drop it into the “Views” group. 

Let’s take a quick look at SwiftUI’s prebuilt shapes. SwiftUI provides us with some basic shapes we can directly use for our views. These are:

  • Rectangle
  • RoundedRectangle
  • Ellipse
  • Circle
  • Capsule 

You already got to know most of them, for instance, when we initialized a Rectangle in our ContentView earlier. You can manipulate these shapes, for example, by applying different colors, adding rounded corners, etc.

If you want to learn more about them, take a look at this tutorial on www.BLCKBIRDS.com.

But we can also define and create our own shapes by using so-called paths. In a nutshell, you can imagine a path like a set of drawing instructions, including lines, curves, and other segments like arcs. This is why a shape is doing nothing else than using a specific path to define its appearance. 

Let’s try to draw a simple square by using such a path. Start with replacing the default Text view in our Chart with a Path instance followed by a corresponding closure. Inside this closure, we can define the way our Path should go. 

By default, SwiftUI will fill out the resulting view with black color. While drawing the Path, however, I prefer to only see the outer border of the resulting view for having a better overview of what I’m drawing. To do this, we can use the .stroke modifier. 

struct Chart: 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” to our imagined square’s upper-right corner. Then, we add a line that 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 preview!

Let’s finish the square by adding a third line pointing to the upper-left corner. We could close the square by adding one last line pointing to where we started, but we can also close the Path “automatically” by using the .closeSubPath modifier. Now that we are done defining our square path, we can fill it out by deleting the .stroke modifier again.

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()

This should result in the following view:

You already know that shapes also consist of paths. Because of this and for reusability purposes, we can simply convert our square Path to such a shape by declaring a struct below our Chart struct, which conforms to the Shape protocol. 

struct MySquare: Shape {
    
}

The only requirement for the Shape protocol is having a path function that 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
    }
}

Now, we can use our MySquare shape inside our SwiftUI view!

struct Chart: 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 to transform it by adding a .frame modifier to the MySquare instance inside our SwiftUI view. 

We can use the rect parameter of our MySquare’s path function for this. The rect is like an invisible scratchpad inside which we can draw our square and can be transformed by passing a specific frame to it when initializing the resulting shape.

So, let’s replace the fixed coordinates with 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
    }
}

This results in the following preview:

Let’s talk through it very quickly. First, we 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 using the rect’s width for both, the x- and y-coordinates (remember, we want to ensure it’s a square). 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!

MySquare()
    .frame(width: 250, height: 250)

If you want, you can delete the MySquare struct and its corresponding instance inside the Chart now since we won’t need it anymore.

Drawing the chart grid 📐

Let’s utilize what we just learned to draw the grid for our chart. The grid should consist of three vertical and three horizontal lines.

Let’s start by creating a new struct called “Grid” that adopts the Shape protocol and initializes a Path

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

In our Chart view, we initialize this Grid shape followed by a light .stroke for immediately seeing what we’re drawing:

struct Chart: View {
    var body: some View {
        Grid()
            .stroke(lineWidth: 0.2)
    }
}

Let’s start drawing the first horizontal line for our Grid. This line should begin here:

The following coordinates can express this in relation to the invisible rect we’re drawing in:

X = 0.25 * “Height of the rect”

Y = 0 * “Height of the rect”

So let’s move our “cursor” to this point by writing:

func path(in rect: CGRect) -> Path {
        
    var path = Path()
        
    path.move(to: CGPoint(x: rect.size.width*0.25, y: 0))
        
    return path
}

Now, we can draw this line by defining the endpoint coordinates like this:

func path(in rect: CGRect) -> Path {
        
    var path = Path()
        
    path.move(to: CGPoint(x: rect.size.width*0.25, y: 0))
    path.addLine(to: CGPoint(x: rect.size.width*0.25, y: rect.size.height))

    return path
}

This is how your preview should look like now:

By using the same technique, we can draw the two remaining horizontal lines for our Grid.

struct Grid: Shape {
    
    func path(in rect: CGRect) -> Path {
        
        var path = Path()
        
        //...
        
        path.move(to: CGPoint(x: rect.size.width*0.5, y: 0))
        path.addLine(to: CGPoint(x: rect.size.width*0.5, y:             rect.size.height))
        
        path.move(to: CGPoint(x: rect.size.width*0.75, y: 0))
        path.addLine(to: CGPoint(x: rect.size.width*0.75, y: rect.size.height))
        
        return path
    }
    
}

Finally, let’s add the corresponding vertical lines to our Grid like this:

struct Grid: Shape {
    
    func path(in rect: CGRect) -> Path {
        
        var path = Path()
        
        //...
        
        path.move(to: CGPoint(x: 0, y: rect.size.height*0.25))
        path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height*0.25))
        
        path.move(to: CGPoint(x: 0, y:rect.size.height*0.5))
        path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height*0.5))
        
        path.move(to: CGPoint(x: 0, y: rect.size.height*0.75))
        path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height*0.75))
        
        return path
    }
    
}

Let’s take a look at the resulting shape. Awesome!

We can now replace the Rectangle in our ContentView with an instance of the Chart struct containing the Grid path…

VStack {
    //...
    Chart()
        .frame(height: 300)
    //...
}
    .navigationTitle("StockX")

… which results in the following preview:

We’ll practice our drawing skills when creating the graph for our chart later on. For now, let’s take a rest and look at what we’ve accomplished so far.

Recap

We’ve prepared the UI for our StockX apps and learned the basics about drawing in SwiftUI. We’ve learned how we can define paths for creating custom shapes. If you want to learn more about drawing in SwiftUI, make sure you check out this tutorial on our website where we discuss this topic in more detail.

Next, we will create the data model for our stock data and generate some random entries we can use to display a sample stock chart graph. By doing this, we will also practice our SwiftUI drawing skills.

Leave a Reply

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