SwiftUI lessons (part 7)

link to part 6

We draw lines, arcs, diagrams

In this section, you'll learn how to draw lines, arcs, and diagrams using Path and built-in shapes like Circle and RoundedRectangle in SwiftUI. Here's what we'll learn:

* Understanding Path and Line Drawing

*What is the Shape protocol and how to draw a custom shape by conforming to the protocol

*Drawing a diagram

* Create a progress bar using an open circle

* Drawing a “pie chart” diagram

We're going to draw a lot of different shapes and thus learn how to use the key Path structure and the Shape protocol.

Understanding Path

In SwiftUI, you draw lines and shapes using Path. If you contact to Apple documentation,Path is a structure containing the outline of a 2D shape. The basic idea is to set a starting point and then draw lines from point to point. Let's look at an example. Let's take figure 2 and look step by step at how this rectangle is drawn.

If you wanted to explain to me how to draw a rectangle step by step, you would probably provide the following description:

  1. Move point to position (20, 20)

  2. Draw a line from (20, 20) to (300, 20)

  3. Draw a line from (300, 20) to (300, 200)

  4. Draw a line from (300, 200) to (20, 200)

  5. Fill the entire area with green

This is exactly how Path works! Let's write your description in code:

import SwiftUI

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

In the above code, we used Path to draw a rectangle and filled it with green color. We moved to the starting point (20, 20), drew lines from point to point to create a rectangle shape, and closed the path using path.closeSubpath(). We then used the .fill modifier to set the color to fill the area green. This is a simple example of using Path to draw vector images in SwiftUI. You can use Path to create more complex shapes and fill them with any color or gradient.

Use Stroke to draw borders

In SwiftUI, you can use Path to draw lines, not just fill areas with color. To do this, you can use the .stroke modifier, which allows you to set the width and color of the line. For example, you can draw the outline of a rectangle using Path and set the line width and color using the .stroke modifier.

In the above code, we used Path to draw a rectangle and filled it with green color. If you just want to draw lines, you can remove the .fill modifier and add a .stroke modifier, specifying the line width and color. For example:

struct ContentView: View {
    var body: some View {
        Path { path in
            path.move(to: CGPoint(x: 20, y: 20))
            path.addLine(to: CGPoint(x: 300, y: 20))
            path.addLine(to: CGPoint(x: 300, y: 200))
            path.addLine(to: CGPoint(x: 20, y: 200))
            path.closeSubpath()
        }
        .stroke(Color.green, lineWidth: 2)
    }
}

Drawing curved lines

In SwiftUI, you can use Path to draw complex shapes such as curves and arcs. To do this, you can use the addQuadCurve, addCurve and addArc methods, which allow you to create curves and arcs. For example, you can draw a dome on top of a rectangle using Path and the addArc method.

In the above code, we have used Path to draw a rectangle and an outline. If you want to add a dome to the top of a rectangle, you can use the addArc method to create an arc connecting the top two points of the rectangle. For example:

struct ContentView: View {
    var body: some View {
        Path { path in
            path.move(to: CGPoint(x: 20, y: 20))
            path.addLine(to: CGPoint(x: 300, y: 20))
            path.addLine(to: CGPoint(x: 300, y: 200))
            path.addLine(to: CGPoint(x: 20, y: 200))
            path.addArc(center: CGPoint(x: 160, y: 200), radius: 140, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 180), clockwise: false)
            path.closeSubpath()
        }
        .stroke(Color.green, lineWidth: 2)
    }
}

In SwiftUI, you can use the addQuadCurve method to draw curves by defining anchor and control points. Anchor points define the start and end of a curve, and a control point defines the shape of the curve. For example, you can draw a corner that extends away from the middle of the line using Path and the addQuadCurve method, defining anchor and control points.

If you want to use the addQuadCurve method to draw corners with control points, you can define anchor and control points as shown below:

struct ContentView: View {
	var body: some View {
		Path { path in
			path.move(to: CGPoint(x: 20, y: 20))
			path.addLine(to: CGPoint(x: 300, y: 20))
			path.addLine(to: CGPoint(x: 300, y: 200))
			path.addLine(to: CGPoint(x: 20, y: 200))
			path.addQuadCurve(to: CGPoint(x: 210, y: 60), control: CGPoint(x: 125, y: 90))
			path.addLine(to: CGPoint(x: 40, y: 60))
			path.closeSubpath()
		}
		.stroke(Color.green, lineWidth: 2)
	}
}

Finally, the addCurve method allows you to draw more complex curves by defining multiple control points. Unlike the addQuadCurve method, which uses a single control point, the addCurve method uses three or more control points to create a curve.

For example, you can draw a wavy line using Path and the addCurve method by defining several control points. In the code below, we used Path to draw a wavy line by defining three control points:

struct ContentView: View {
	var body: some View {
		Path { path in
			path.move(to: CGPoint(x: 20, y: 200))
			path.addCurve(to: CGPoint(x: 120, y: 100),
						  control1: CGPoint(x: 70, y: 250), control2: CGPoint(x: 100, y: 50))
			path.addCurve(to: CGPoint(x: 220, y: 200),
						  control1: CGPoint(x: 140, y: 150), control2: CGPoint(x: 180, y: 250))
			path.addCurve(to: CGPoint(x: 320, y: 100),
						  control1: CGPoint(x: 260, y: 150), control2: CGPoint(x: 290, y: 50))
		}
		.stroke(Color.green, lineWidth: 2)
	}
}

Fill and Stroke

What if you want to draw the border of a shape and fill the shape with color at the same time? The fill and stroke modifiers cannot be used in parallel. You can use ZStack to achieve the desired effect. Here's the code:

struct ContentView: View {
	var body: some View {
		ZStack {
			// Draw the filled shape
			Path { path in
				path.move(to: CGPoint(x: 20, y: 20))
				path.addLine(to: CGPoint(x: 200, y: 20))
				path.addQuadCurve(to: CGPoint(x: 110, y: 100), control: CGPoint(x: 200, y: 90))
				path.addLine(to: CGPoint(x: 20, y: 20))
			}
			.fill(Color.purple)

			// Draw the border of the shape
			Path { path in
				path.move(to: CGPoint(x: 20, y: 20))
				path.addLine(to: CGPoint(x: 200, y: 20))
				path.addQuadCurve(to: CGPoint(x: 110, y: 100), control: CGPoint(x: 200, y: 90))
				path.addLine(to: CGPoint(x: 20, y: 20))
			}
			.stroke(Color.black, lineWidth: 4)
		}
	}
}

In SwiftUI, you can use ZStack to stack one Path object on top of another to draw the border of a shape and fill the shape with color at the same time. In the above code, we have created two Path objects with the same path and stacked one on top of the other using ZStack. The bottom Path object uses the fill modifier to fill the domed rectangle with purple. The top Path object uses the stroke modifier to draw only the borders in black. This is a simple example of using ZStack to draw the border of a shape and fill the shape with color at the same time in SwiftUI. You can use Path to create more complex shapes and use ZStack to stack one Path object on top of another to draw the border of a shape and fill the shape with color at the same time.

Drawing arcs and diagrams

SwiftUI provides a convenient API for developers to draw arcs. This API is very useful for creating various shapes and objects, including pie charts. To draw an arc, you write code like this:

struct ContentView: View {
	var body: some View {
		Path { path in
			path.move(to: CGPoint(x: 200, y: 200))
			path.addArc(center: .init(x: 200, y: 200), radius: 150, startAngle: .degrees(0), endAngle: .degrees(90), clockwise: true)
		}
		.fill(.green)
	}
}

In this code, we first move to the point (200, 200) using the method move(to:). Then we add an arc using the method addArc(center:radius:startAngle:endAngle:clockwise:). As parameters we pass the arc center, radius, start and end angles, as well as the direction of drawing the arc (clockwise or counterclockwise). In this case, we draw an arc from 0 to 90 degrees clockwise and fill it with green using the modifier .fill(.green).

With the addArc function, you can easily create a pie chart with colorful segments. To do this, simply overlay different segments of a pie chart on top of each other using ZStack. Each segment has different values ​​for startAngle and endAngle to make the diagram. Here's an example:

struct ContentView: View {
	var body: some View {
		ZStack {
			Path { path in
				path.move(to: CGPoint(x: 187, y: 187))
				path.addArc(center: .init(x: 187, y: 187),
							radius: 150, startAngle: .degrees(0),
							endAngle: .degrees(190), clockwise: true)
			}
			.fill(.yellow)
			Path { path in
				path.move(to: CGPoint(x: 187, y: 187))
				path.addArc(center: .init(x: 187, y: 187),
							radius: 150, startAngle: .degrees(190),
							endAngle: .degrees(110), clockwise: true)
			}
			.fill(.teal)
			Path { path in
				path.move(to: CGPoint(x: 187, y: 187))
				path.addArc(center: .init(x: 187, y: 187),
							radius: 150, startAngle: .degrees(110),
							endAngle: .degrees(90), clockwise: true)
			}
			.fill(.blue)
			Path { path in
				path.move(to: CGPoint(x: 187, y: 187))
				path.addArc(center: .init(x: 187, y: 187),
							radius: 150, startAngle: .degrees(90),
							endAngle: .degrees(360), clockwise: true)
			}
			.fill(.purple)
		}
	}
}

This code creates a pie chart with four segments. Each segment is specified using the `Path` function, which creates a path consisting of a single arc. Then the `fill` function is applied to each path, which fills the path with a specific color.

The center of the circle is given by `CGPoint(x: 187, y: 187)`, which corresponds to the center of the screen in this case. Each segment is specified using the `addArc` function, which takes the parameters `center`, `radius`, `startAngle` and `endAngle`. Here the radius is 187, and the angles are specified in degrees.

The first segment is shaded yellow and occupies 190 degrees from 0 to 190. The second segment is shaded blue and occupies 80 degrees from 190 to 110. The third segment is shaded blue and occupies 20 degrees from 110 to 90. The fourth segment is shaded purple and occupies 270 degrees from 90 to 360.

The result is a pie chart in which each segment occupies a certain percentage of the total circle.

In some cases, you may need to highlight a specific segment by separating it from the pie chart. For example, to highlight a purple segment, you can apply the modifier offsetto offset the segment:

struct ContentView: View {
	var body: some View {
		ZStack {
			Path { path in
				path.move(to: CGPoint(x: 187, y: 187))
				path.addArc(center: .init(x: 187, y: 187),
							radius: 150, startAngle: .degrees(0),
							endAngle: .degrees(190), clockwise: true)
			}
			.fill(.yellow)
			Path { path in
				path.move(to: CGPoint(x: 187, y: 187))
				path.addArc(center: .init(x: 187, y: 187),
							radius: 150, startAngle: .degrees(190),
							endAngle: .degrees(110), clockwise: true)
			}
			.fill(.teal)
			Path { path in
				path.move(to: CGPoint(x: 187, y: 187))
				path.addArc(center: .init(x: 187, y: 187),
							radius: 150, startAngle: .degrees(110),
							endAngle: .degrees(90), clockwise: true)
			}
			.fill(.blue)
			Path { path in
			path.move(to: CGPoint(x: 187, y: 187))
			path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90
			), endAngle: .degrees(360), clockwise: true)
			}
			.fill(.purple)
			.offset(x: 20, y: 20)
		}
	}
}

This example moves the purple segment 20 pixels on the X axis and 20 pixels on the Y axis to make it stand out from the pie chart. This effect can be useful when you want to highlight or provide additional information about a specific segment.

Optionally, you can add a border to further attract people's attention. If you want to add a label to the highlighted segment, you can also overlay a text view like this:

struct ContentView: View {
	var body: some View {
		ZStack {
			Path { path in
				path.move(to: CGPoint(x: 187, y: 187))
				path.addArc(center: .init(x: 187, y: 187),
							radius: 150, startAngle: .degrees(0),
							endAngle: .degrees(190), clockwise: true)
			}
			.fill(.yellow)
			Path { path in
				path.move(to: CGPoint(x: 187, y: 187))
				path.addArc(center: .init(x: 187, y: 187),
							radius: 150, startAngle: .degrees(190),
							endAngle: .degrees(110), clockwise: true)
			}
			.fill(.teal)
			Path { path in
				path.move(to: CGPoint(x: 187, y: 187))
				path.addArc(center: .init(x: 187, y: 187),
							radius: 150, startAngle: .degrees(110),
							endAngle: .degrees(90), clockwise: true)
			}
			.fill(.blue)
			Path { path in
			path.move(to: CGPoint(x: 187, y: 187))
			path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90
			), endAngle: .degrees(360), clockwise: true)
			}
			.fill(.purple)
			.offset(x: 20, y: 20)
			Path { path in
			path.move(to: CGPoint(x: 187, y: 187))
			path.addArc(center: .init(x: 187, y: 187), radius: 150, startAngle: .degrees(90
			), endAngle: .degrees(360), clockwise: true)
				path.closeSubpath()
			}
			.stroke(Color(red: 52/255, green: 52/255, blue: 122/255), lineWidth: 10) .offset(x: 20, y: 20)
			.overlay(
				Text("25%")
					.font(.system(.largeTitle, design: .rounded))
					.bold()
					.foregroundColor(.white)
					.offset(x: 80, y: -110)
			)
		}
	}
}

Understanding the Shape protocol

Before learning the Shape Protocol, let's start with a simple exercise. Based on what you've learned so far, draw the following shape using Path.

Don't look at the solution right away. Try it yourself.

Ok, to make this shape you create a path using addRect and addQuadCurve:

struct ContentView: View {
	var body: some View {
			Path() { path in
			path.move(to: CGPoint(x: 100, y: 100))
			path.addQuadCurve(to: CGPoint(x: 300, y: 100), control: CGPoint(x: 200, y: 80))
			path.addRect(CGRect(x: 100, y: 100, width: 200, height: 40))
			}
			.fill(Color.green)

	}
}

This code creates a path that starts at the point (100, 100). Then a quadratic Bezier curve is added that goes through the point (200, 80) and ends at the point (300, 100). Then a 200×40 rectangle is added at the bottom of the curve. The path is then filled with green.

This code creates the shape shown in the figure. You can experiment with the Curve and Rectangle options to change the shape of the shape.

Let's talk about the Shape protocol. The protocol is very simple and contains only one requirement. To implement it, you need to implement the following function:

func path(in rect: CGRect) -> Path

When is it useful to implement the Shape protocol? To answer this question, let's say you want to create a button with a dome shape, but with a dynamic size. Is it possible to reuse the path you just created?

Look again at the code above. You have created a path with absolute coordinates and size. To create the same shape, but with a variable size, you can create a structure that implements the Shape protocol and implement the path(in:) function. When the path(in:) function is called by the framework, you will be given the size of the rectangle. You can then draw a path inside this rectangle.

Let's create the following structure:

struct Dome: Shape {
	func path(in rect: CGRect) -> Path {
		var path = Path()
		path.move(to: CGPoint(x: 0, y: 0))
		path.addQuadCurve(to: CGPoint(x: rect.size.width, y: 0), control: CGPoint( x: rect.size.width/2, y: -(rect.size.width * 0.1)))
		path.addRect(CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height))
		return path }
}

And add a button with this form:

struct ContentView: View {
	var body: some View {
		Button {
			
		} label: {
			Text("Dome Button")
				.font(.system(.title, design: .rounded))
				.bold()
				.foregroundColor(.white)
				.frame(width: 250, height: 50)
				.background(Dome().fill(Color.red))
		}
	}
}

We use the dome shape as the background of the button. Its width and height are based on the specified frame size.

We use built-in Shapes (forms)

We created a custom shape using the Shape protocol, but SwiftUI actually comes with several built-in shapes, including Circle, Rectangle, RoundedRectangle, Ellipse, etc. If you don't need anything complicated, these shapes are good enough for creating basic objects.

Let's create such an improvised stop button

struct ContentView: View {
	var body: some View {
		Circle()
			.foregroundColor(.green)
			.frame(width: 200, height: 200)
			.overlay(
				RoundedRectangle(cornerRadius: 5)
					.frame(width: 80, height: 80)
					.foregroundColor(.white)
		)
	}
}

As you can see, it was enough for us to simply get the RoundedRectangle which was kindly provided to us in the SUI framework

Creating a progress bar using Shapes

By mixing and matching built-in shapes, you can create many different types of vector-based UI controls for your applications. Let's look at another example. The following screenshot demonstrates how to construct a progress bar using a circle.

This progress bar is a combination of two circles. At the base there is a gray outline of a circle. Above it is an open outline of a circle, symbolizing the progress of execution. In your project you need to write the code in ContentView like this:

struct ContentView: View {
	private var purpleGradient = LinearGradient(
		gradient: Gradient(
			colors: [ Color (red: 207/255, green: 150/255, blue: 207/255),
					  Color(red: 107/255, green: 116/255,
							blue: 179/255) ]),
		startPoint: .trailing, endPoint: .leading)
	
	var body: some View {
		ZStack {
			Circle()
				.stroke(Color(.systemGray6), lineWidth: 20)
				.frame(width: 300, height: 300)
		} }
}

The stroke modifier is used to display the outline of the gray circle. Depending on your preferences, you can adjust the lineWidth parameter to change the thickness of the lines.

The purpleGradient property specifies the purple gradient that will be used when drawing the open circle.

Now let's add some code that will add a circle that will show the filling:

struct ContentView: View {
	private var purpleGradient = LinearGradient(
		gradient: Gradient(
			colors: [ Color (red: 207/255, green: 150/255, blue: 207/255),
					  Color(red: 107/255, green: 116/255,
							blue: 179/255) ]),
		startPoint: .trailing, endPoint: .leading)
	
	var body: some View {
		ZStack {
			
			Circle()
				.stroke(Color(.systemGray6), lineWidth: 20)
				.frame(width: 300, height: 300)
			Circle()
				.trim(from: 0, to: 0.91)
				.stroke(purpleGradient, lineWidth: 20)
				.frame(width: 300, height: 300)
				.overlay {
					VStack {
						Text("91%")
							.font(.system(size: 80, weight: .bold, design: .rounded))
								.foregroundColor(.gray)
						Text("Complete")
							.font(.system(.body, design: .rounded))
							.bold()
							.foregroundColor(.gray)
					}
				}
		}
	}
}

To create an open circle, you must add a trim modifier, specifying the from and to values ​​to determine which segment of the circle should be displayed. In this case we want to demonstrate progress of 91%. To do this, the from value is set to 0 and the to value to 0.91.

To display the percentage complete, we centered the text view in the circle.

Drawing a Donut Chart

The last example I would like to show you is a donut chart. If you already understand how the trim modifier works, you probably already know how we're going to implement a ring chart. By changing the values ​​of the trim modifier, we can divide the circle into several segments. This is the technique we use to create a donut chart, and here is the code:

struct ContentView: View {
	var body: some View {
		ZStack {
			Circle()
				.trim(from: 0, to: 0.2)
				.stroke(Color(.systemBlue), lineWidth: 80)
			Circle()
				.trim(from: 0.2, to: 0.43)
				.stroke(Color(.systemTeal), lineWidth: 80)
			Circle()
				.trim(from: 0.43, to: 0.70)
				.stroke(Color(.systemPurple), lineWidth: 80)
			Circle()
				.trim(from: 0.70, to: 1)
				.stroke(Color(.systemGreen), lineWidth: 90)
				.overlay(
					Text("30%")
						.font(.system(.title, design: .rounded))
						.bold()
						.foregroundColor(.white)
						.offset(x: 80, y: -100)
				)
		}
		.frame(width: 250, height: 250)
	}
}

This code creates a donut chart with four segments of different colors. Each segment is a circle that has a trim modifier applied to it to divide it into a specific segment.

After that, each segment is painted with a specific color using the stroke modifier. The line width of each segment is specified using the lineWidth parameter.

At the end of the code is a text view that appears on top of the last segment and shows the percentage. The text is offset using the offset modifier.

In summary, this code produces a ring chart with four segments separated by the trim modifier, and a text view that displays the percentages.

Results:

We looked at how you can use Path and Shape to create various forms of your own or how to take those that Apple gives us in SUI, I think the most attentive of you – those who follow how this framework is developing know that there is such a Charts module using which you can draw various graphs and it is more convenient than using Path and Shape, for example like this:

import SwiftUI
import Charts

struct DataPoint: Identifiable {
	let id: String
	let value: Double
}



let data: [DataPoint] = [
	DataPoint(id: "January", value: 23.0),
	DataPoint(id: "February", value: 45.0),
	DataPoint(id: "March", value: 32.0),
	DataPoint(id: "April", value: 56.0),
	DataPoint(id: "May", value: 28.0),
	DataPoint(id: "June", value: 70.0)
]

struct ContentView: View {
	var body: some View {
		Chart(data) { dataPoint in
			BarMark(
				x: .value("Month", dataPoint.id),
				y: .value("Value", dataPoint.value)
			)
		}
	}
}

However, you need to understand that in this part we were not working on graphs, but we were studying how you can generally draw various shapes, and I hope that you learned this by reading this article!

As before, subscribe to my telegram channel – https://t.me/swiftexplorer

The next parts of SwiftUI tutorials will be released soon.

Thanks for reading!

Similar Posts

Leave a Reply

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