This Swift package can be used to easily build SwiftUI shapes that conform to the Shape
protocol. Most of the well-known shapes are in here, and some not-so-well-known shapes are here too!
Shapes requires a minimum of macOS 12, iOS 15, and tvOS 15.
Most of the types that are defined in this project are structs that adhere to SwiftUI's Shape
protocol. Because Shape
is already taken, I settled on NFiShape
as a base protocol that all shapes must conform to.
public protocol NFiShape: InsettableShape {
var inset: CGFloat { get set }
}
The definition shows that NFiShape
conforms to InsettableShape
, which allows us to use insets on our shapes. ;)
One of the great things about defining NFiShape
as a protocol is that we can implement a protocol extension that covers our bases as it relates to InsettableShape
:
public extension NFiShape {
func inset(by amount: CGFloat) -> some InsettableShape {
var me = self
me.inset += amount
return me
}
}
Now any shape that in turn conforms to NFiShape
will automatically get this behavior for free.
A Polygon
is a closed shape that consists of multiple line segments. It is defined in this project as a protocol:
/// A plane figure that is described by a finite number of straight line segments connected to form a closed polygonal chain
public protocol Polygon: NFiShape {
var sides: Int { get }
func vertices(in rect: CGRect) -> [CGPoint]
}
Each Polygon
is defined by the number of sides. In order for the shape to be drawn, the vertices
of each polygon are also required. These are managed via a CGPoint
array. The vertices are obtained with a method in order to determine the position given a CGRect
Not all shapes in this library will conform to Polygon
because they may include curved surfaces. However, all shapes should conform to NFiShape
to take advantage of the insettable feature.
The simplest Polygon example in the library is appropriately named SimplePolygon
. Provide an array of ratios in the initializer to construct a polygon with points around the unit circle that are contained in the shape's frame.
For example, if one were to define a polygon with points at 0.33, 0.67, and 1:
SimplePolygon(ratios: [0.33, 0.67, 1])
this tells the library to generate a polygon with vertices at 33%, 67%, and 100% distance traveled around the unit circle. (0% starts at the point facing east in the following illustration)
The code used to generate this diagram is
struct SimplePolygon_Previews: PreviewProvider {
static var previews: some View {
ZStack {
Circle().stroke() // the unit circle
SimplePolygon(ratios: [0.33, 0.67, 1])
.foregroundColor(Color.blue)
}
.border(Color.purple)
}
}
You can provide as many ratios as desired, in any order, and the shape will render them appropriately:
struct SimplePolygon2_Previews: PreviewProvider {
static var previews: some View {
ZStack {
Circle().stroke() // the unit circle
SimplePolygon(ratios: [0.1, 0.5, 0.756, 0.3, 0.4])
.foregroundColor(Color.blue)
}
.border(Color.purple)
}
}
NOTE: For non-square frames, the polygon will be rendered in the center of the frame. The unit circle is measured based on the smallest dimension of the CGRect
:
struct SimplePolygon3_Previews: PreviewProvider {
static var previews: some View {
ZStack {
Circle().stroke()
SimplePolygon(ratios: [0.1, 0.5, 0.756, 0.3, 0.4])
.foregroundColor(Color.blue)
}
.border(Color.purple)
.frame(width: 256, height: 128)
}
}
A convex polygon, also known as a Regular Polygon, is a polygon whose sides are all equal length and whose vertices are equally distributed around the unit circle.
ConvexPolygon
is defined as a struct that conforms to the RegularPolygon
protocol. As a protocol, RegularPolygon
requires the following properties and methods:
/// A polygon with equal-length sides and equally-spaced vertices along a circumscribed circle
public protocol RegularPolygon: Polygon {}
That's right, it's empty! RegularPolygon
conforms to Polygon
without any additional requirements. The reason that RegularPolygon
exists is to implement the vertices(in: CGRect)
method via a protocol extension:
public extension RegularPolygon {
/// obtains the equally spaced points of a polygon around a circle inscribed in rect, arranged clockwise starting at the top of the unit circle
func vertices(in rect: CGRect) -> [CGPoint] {
vertices(in: rect, offset: .zero)
}
/// obtains the equally spaced points of a polygon around a circle inscribed in rect, arranged clockwise starting at the top of the unit circle, with any additional rotational offset
func vertices(in rect: CGRect, offset: Angle = .zero) -> [CGPoint] {
let r = min(rect.size.width, rect.size.height) / 2
let origin = CGPoint(x: rect.midX, y: rect.midY)
let array: [CGPoint] = Array(0 ..< sides).map {
let theta = 2 * .pi * CGFloat($0) / CGFloat(sides) + CGFloat(offset.radians) - CGFloat.pi / 2 // pointing north!
return CGPoint(x: origin.x + r * cos(theta), y: origin.y + r * sin(theta))
}
return array
}
}
In the code above, I made the conscious choice to rotate the origin of the unit circle to be facing up. Most polygon drawings I've seen in textbooks and the internet (no citation provided, sorry) show a corner pointing to the top of the image, so that's the convention that is used here.
The second method, vertices(in: CGRect, offset: Angle)
allows the user to specify additional rotational offset as desired.
These methods are useful to obtain the vertices for other shapes that are not polygons as well, e.g. ReuleauxPolygon
and Torx
with their curved sides.
Now that we understand how RegularPolygon
is used, it's time to learn about ConvexPolygon
. The initializer takes a number of sides, like so:
ConvexPolygon(sides: 7)
This shape can be illustrated with the following example:
struct ConvexPolygon_Previews: PreviewProvider {
static var previews: some View {
ZStack {
Circle() // unit circle
.strokeBorder(Color.red, lineWidth: 10)
ConvexPolygon(sides: 7)
.strokeBorder(Color.green.opacity(0.8), lineWidth: 10)
}
}
}