Home About Contact

 

How to create custom Shapes in SwiftUI

6 min read

SwiftUI comes with quite a number of built-in 2D shapes such as Rectangle, Circle, Ellipse, Capsule, RoundedRectangle etc. If all these are not enough to fulfil your needs, you can also create your own custom shapes by using Path

To create a custom shape using Path is easy, such as follows, which you can just straightaway use the provided Path and it provides you a closure with a parameter, which you can call a number of its methods such as addArc, addCurve, addRect, addLine etc to create a complete path.

import SwiftUI
struct ContentView{
    var body : some View {
        Path { path in 
            path.move(to: CGPoint(x : 200, y : 100))
            path.addLine (to : CGPoint(x: 300, y : 200))
            path.addLine (to : CGPoint(x: 100, y : 200))
            path.addLine (to : CGPoint(x: 200, y : 100))
        }
        .fill(Color.green)
    }
}

Basically, the above simply builds a triangle. First of all, we need the path to start at a point, which we use its move(to:) method to move to a point to start with. The point is an instance of CGPoint which is from CoreGraphics framework.

And then we add the 3 lines that make up the 3 edges of the triangle by invoking the method addLine(to:). The following diagram is just good enough to illustrate the orders of the lines added to form the triangle.

Green triangle adding point sequence

Create your own custom shapes for reusability

The SwiftUI comes with the Shape protocol, which you just need to implement a single method to build your own custom shapes.

The following code shows how you can implement the path(in:) method of the Shape protocol to build a reusable triangle shape.

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

        path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))

        return path
    }
}

In the above, inside the path method, you can instantiate a Path and invokes its move(to:) to start at a point.

The path method provides a parameter rect which is a CGRect that provides a number of useful properties such as :

  • The minimum values of X and Y (minX and minY) in the coordinates
  • The maximum values of X and Y (maxX and maxY)
  • And the respective mid point values of X and Y (midX and midY).

Once you’ve got your own custom shape such as the above, a triangle, you can then use it in any of your SwiftUI view, as follows:

import SwiftUI
struct ContentView : View {

   var body : some View {

        Triangle()
        .stroke(Color.orange, 
             style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .miter))
        .frame(width: 200, height: 200, alignment: .center)
         
   }
}

The result is as follows – a triangle with an orange color stroke :

Create a Polygon shape

A polygon can be created with the following code, which you can then use it to create any polygon with 3 sides and above such as a triangle, a Pentagon (5-sided polygon), a Hexagon (6-sided) etc.

struct Polygon : Shape {
   var sides : Int = 5 

   func path(in rect : CGRect ) -> Path{
        // get the center point and the radius
        let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
        let radius = rect.width / 2  

        // get the angle in radian, 
        // 2 pi divided by the number of sides
        let angle = Double.pi * 2 / Double(sides)
        var path = Path()
        var startPoint = CGPoint(x: 0, y: 0)
        
        for side in 0 ..< sides {
           
            let x = center.x + CGFloat(cos(Double(side) * angle)) * CGFloat (radius)
            let y = center.y + CGFloat(sin(Double(side) * angle)) * CGFloat(radius)
                  
            let vertexPoint = CGPoint( x: x, y: y)
            
            if (side == 0) {
                startPoint = vertexPoint
                path.move(to: startPoint )
            }
            else {
                path.addLine(to: vertexPoint)
            }
          
            // move back to starting point
            // needed for stroke
            if ( side == (sides - 1) ){
                path.addLine(to: startPoint)    
            }
        }
        
        return path
  }
}

The above code is simply some back to school trigonometry. First of all, we need to get the central angle of the Polygon by dividing the circle (2 pi) by the number of sides or angles.

And then we can iterate through the sides and calculate the x and y coordinate of each vertex or corner.

The x and y of each vertex can be calculated by (on line 17 and 18) :

  • Firstly, get the ratio of sides - the adjacent and the opposite by the cosine and sine of each angle respectively.
    The x and y distance from the center can be calculated by multiplying the radius by the adjacent and opposite ratios respectively.
  • Then add the x and y distance with the center x and y to get the coordinate of each vertex.

The following diagram shall illustrate clearly :

trigonometry to create SwiftUI Polygon

Please note that we use move(to:) to add the first coordinate to start with, and then addLine(to:) for other points. And we also store the starting point as we need to add a line back to the starting point to close the path.

So, with the above custom Polygon shape, you can simply use it anywhere in your SwiftUI view to create any polygon shapes, such as triangle, Pentagon, Hexagon by simply specifying the number of sides, as follows:

import SwiftUI
struct ContentView : View {
    var body : some View {
        HStack(spacing :10) {

             Polygon(sides : 3) // Triangle with 3 sides
             .fill(Color.red)
             .frame(width: 100)
      
             Polygon() // Pentagon (default is 5 sides)
             .stroke(Color.green, lineWidth: 3)
             .frame(width : 100)
     
             Polygon(sides : 6 ) // Hexagon
             .fill(Color.blue)
             .frame(width : 100)
        }
    }
}

The following is how it's shown on the Xcode preview :

Polygon, hexagon, pentagon, triangle SwiftUI

A Star shape

The following code can be used to create any star shape with 3 points and above.

import SwiftUI
struct Star : Shape {
    var points : Int = 5 // If it's not specified it's a 5-point star
    var innerRatio : Double = 0.2
     
    func path(in rect : CGRect) -> Path {
     
        let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
        let angle = Double.pi / Double(points)
        var path = Path() 
        var startPoint = CGPoint(x: 0, y: 0)
 
        let outerRadius = center.x
        let innerRadius = outerRadius * innerRatio
        
        let maxCorners = 2 * points

        for corner in 0 ..< maxCorners {
            
            let radius = (corner % 2) == 0 ? outerRadius : innerRadius
            
            let x = center.x + CGFloat(cos(Double(corner) * angle)) * CGFloat (radius)
            let y = center.y + CGFloat(sin(Double(corner) * angle)) * CGFloat(radius)
            let point = CGPoint( x: x, y: y)
            
            if (corner == 0) {
                startPoint = point // store the starting point
                path.move(to:point )
            }
            else {
                path.addLine(to: point)
            }     
            // move back to starting point
            if ( corner == (maxCorners - 1) ){ 
                path.addLine(to: startPoint)     
            }
        }
        
        return path
    }
}

Basically, the above code is more or less the same with the code used to create a Polygon, except the Star shape has double the corners or points that a Polygon has i.e. each point (or corner) of a Star will have one inner corner or point.

So, the following code is an example on creating a few star shapes and arranged in a HStack in a SwiftUI view.

struct ContentView: View {
    var body: some View {
        
        HStack {
            
            Star(points: 3)
            .fill(Color.green)
            .frame(width:100)
            
            Star(points: 4)
            .fill(Color.blue)
            .frame(width:100)
            
            Star(points: 5, innerRatio: 0.5)
            .fill(Color.orange)
            .frame(width:100)
            
            Star(points: 6, innerRatio: 0.35)
            .stroke(Color.red, lineWidth: 3)
            .frame(width:100)
        }
    }
}

The parameter innerRatio is to control the sharpness of the star's corners, the lower the value the sharper it is.

Spread the love
Posted on May 4, 2021 By Christopher Chee

Please leave us your comments below, if you find any errors or mistakes with this post. Or you have better idea to suggest for better result etc.

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


Our FB Twitter Our IG Copyright © 2021