A Swift Package that makes SwiftUI shapes easier to make by redefining them as an array of styled corners. (The logo above was created in 100 lines + SwiftUI Text)
Features:
Corner
, aCGPoint
withstyle
.CornerStyle
options:.point
,.rounded
,.straight
,.cutout
, and.concave
- Basic shapes like
CornerRectangle
,CornerTriangle
, andCornerPentagon
with stylable corners. CornerShape
, a protocol for making your own open or closed shapes out of an array of Corners.CornerCustom
, for building corner shapes inline without making a new type.- Add a
Notch
of anyNotchStyle
inbetween two corners. .addOpenCornerShape()
or.addClosedCornerShape()
for adding a few corners to a SwiftUIPath
Vector2
, a new type similar toCGPoint
but used to do vector math.Vector2Representable
protocol that adds a.vector
property needed to conform to other Vector2-related protocols.Vector2Algebraic
protocol used to add vector algebra capabilities toVector2
Vector2Transformable
protocol with methods for transforming arrays of points.RectAnchor
, an enum for all major anchor points on a rectangle. Used for transform functions.RelatableValue
, an enum used to store.relative
or.absolute
values.SketchyLine
, an animatable lineShape
that aligns to frame edges and can extend beyond the frame..emboss()
or.deboss()
any SwiftUIShape
orView
.AnimatablePack
as an alternative toAnimatablePair
that takes any number of properties.
The Example
folder has an app that demonstrates the features of this package.
This package is compatible with iOS 14+, macOS 11+, watchOS 7+, tvOS 14+, and visionOS.
- In Xcode go to
File -> Add Packages
- Paste in the repo's url:
https://github.com/ryanlintott/ShapeUp
and select by version. - Import the package using
import ShapeUp
Really it's up to you. I currently use this package in my own Old English Wordhord app.
Additionally, if you find a bug or want a new feature add an issue and I will get back to you about it.
ShapeUp is open source and free but if you like using it, please consider supporting my work.
Or you can buy a t-shirt with the ShapeUp logo
A point with a specified CornerStyle
used to draw paths and create shapes.
Corner(.rounded(radius: 5), x: 0, y: 10)
Corners store no information about their orientation or where the previous and next points are located. When they're put into an array their order is assumed to be their drawing order. This means you can generate a Path
from this array. By default this path is assumed to be closed with the last point connecting back to the first.
[
Corner(.rounded(radius: 10), x: 0, y: 0),
Corner(.cutout(radius: 5), x: 10, y: 0),
Corner(.straight(radius: 5), x: 5, y: 10)
].path()
An enum storing style information for a Corner
. In all cases, the radius is a RelatableValue
that can either be an absolue value or relative to the length of the shortest line from that corner.
.point
A simple point corner with no properties.
.rounded(radius: RelatableValue)
A rounded corner with a radius.
.concave(radius: RelatableValue, radiusOffset: CGFloat)
A concave corner where the radius determines the start and end points of the cut and the radius offeset is the difference between the concave radius and the radius. The radiusOffset is mainly used when insetting a concave corner and is often left with the default value of zero.
.straight(radius: RelatableValue, cornerStyles: [CornerStyle] = [])
A straight chamfer corner where the radius determines the start and end points of the cut. Additional cornerstyles can be used on the two resulting corners of the chamfer. (You can continue nesting recursively.)
.cutout(radius: RelatableValue, cornerStyles: [CornerStyle] = [])
A cutout corner where the radius determines the start and end points of the cut. Additional cornerstyles can be used on the three resulting corners of the cut. (Again, you can continue nesting recursively.)
CornerRectangle
, CornerTriangle
, and CornerPentagon
are pre-built shapes where you can customize the style of any corner.
Examples:
CornerRectangle([
.topLeft: .straight(radius: 60),
.topRight: .cutout(radius: .relative(0.2)),
.bottomRight: .rounded(radius: .relative(0.8)),
.bottomLeft: .concave(radius: .relative(0.2))
])
.fill()
CornerTriangle(
topPoint: .relative(0.6),
styles: [
.top: .straight(radius: 10),
.bottomRight: .rounded(radius: .relative(0.3)),
.bottomLeft: .concave(radius: .relative(0.2))
]
)
.stroke()
CornerPentagon(
pointHeight: .relative(0.3),
topTaper: .relative(0.1),
bottomTaper: .relative(0.3),
styles: [
.topRight: .concave(radius: 30),
.bottomLeft: .straight(radius: .relative(0.3))
]
)
.fill()
A protocol for creating shapes built from an array of Corner
s. The path and inset functions needed to conform to SwiftUI InsettableShape are already implemented.
- Set insetAbount zero (this property is used if the shape inset).
- Set the closed property to define if your shape should be closed or left open.
- Write a function that returns an array of corners.
public struct MyShape: CornerShape {
public var insetAmount: CGFloat = .zero
public let closed = true
public func corners(in rect: CGRect) -> [Corner] {
[
Corner(x: rect.midX, y: rect.minY),
Corner(.rounded(radius: 5), x: rect.maxX, y: rect.maxY),
Corner(.rounded(radius: 5), x: rect.minX, y: rect.maxY)
]
}
}
A CornerShape
can be used in SwiftUI Views the same way as RoundedRectangle
or similar.
MyShape()
.fill()
The corners can also be accessed directly for use in a more complex shape
public func corners(in rect: CGRect) -> [Corner] {
MyShape()
.corners(in: rect)
.inset(by: 10)
.addingNotch(Notch(.rectangle, depth: 5), afterCornerIndex: 0)
}
Sometimes you might want to make a shape inline without defining a new struct. CornerCustom
is a CornerShape
that takes a closure that returns an array of Corner
s. The closure itself needs to be Sendable
so that it can be used to generate a path for a SwiftUI Shape
.
CornerCustom { rect in
[
Corner(x: rect.midX, y: rect.minY),
Corner(.rounded(radius: 10), x: rect.maxX, y: rect.maxY),
Corner(.rounded(radius: 10), x: rect.minX, y: rect.maxY)
]
}
.strokeBorder(lineWidth: 5)
Sometimes you want to cut a notch in the side of a shape. This can be tricky to do when the line is at an odd angle but Notch
makes it easy. A Notch
has a NotchStyle
, position, length and depth.
The following code adds a rectangular notch between the second and third corner. The addingNotch()
function makes all the necessary calculations to add the corners representing that notch into the Corner
array.
let notch = Notch(.rectangle, position: .relative(0.5), length: .relative(0.2), depth: .relative(0.1))
let corners = corners.addingNotch(notch, afterCornerIndex: 1)
Two basic styles are .triangle
and .rectangle
and both allow customization of the corner styles for the 2 or 3 resulting notch corners.
/// Specify styles for each corner
let notch1 = .triangle(cornerStyles: [.rounded(radius: 10), .point, .straight(radius: 5)])
/// Or specify one style for all
let notch2 = .rectangle(cornerStyle: .rounded(radius: .relative(0.2))
There is also a .custom
notch style that takes a closure that returns an array of Corner
s based on a CGRect
that matches the size and orientation of the notch.
let notch = .custom { rect in
[
Corner(x: rect.minX, y: rect.minY),
Corner(x: rect.minX, y: rect.midY),
Corner(.rounded(radius: 15), x: rect.midX, y: rect.maxY),
Corner(x: rect.maxX, y: rect.midY),
Corner(x: rect.maxX, y: rect.minY)
]
}
Shapes made completely with corners have their limitations. Only straight lines and arcs are possible. If you want to use corners to draw only a portion of your shape you can do that too with .addOpenCornerShape()
and .addClosedCornerShape()
functions added to Path
A vector type used as an alternative to CGPoint that conforms to all the Vector2 protocols.
A protocol that adds the vector: Vector2
property. Vector2
, CGPoint
, and Corner
all conform to this and it's required for any other Vector2 protocols.
Other properties and methods:
point: CGPoint
corner(_ style:) -> Corner
Array extensions:
points: [CGPoint]
vectors: [Vector2]
corners(_ style: CornerStyle?) -> [Corner]
corners(_ styles: [CornerStyle?]) -> [Corner]
bounds: CGRect
center: CGPoint
anchorPoint(_ anchor: RectAnchor) -> CGPoint
angles: [Angle]
A protocol that adds vector math. Only applied to Vector2
by default but can be added to any other Vector2Representable
if need be.
Functions include: magnitude, direction, normalized, addition, subtraction, and multiplication or division with scalars.
A protocol that adds transformation functions (move, rotate, flip, inset) to any Vector2Representable
or array of that type. Applied to Vector2
, CGPoint
, and Corner
.
An enum to indicate one of 9 anchor locations on a rectangle. It's primarily used to quickly get CGPoint
values from CGRect
// Current method
let point = CGPoint(x: rect.minX, y: rect.minY)
// ShapeUp method
let point = rect.point(.topLeft)
This is especially helpful when getting an array of points
// Current method
let points = [
CGPoint(x: rect.minX, y: rect.midY),
CGPoint(x: rect.midX, y: rect.midY),
CGPoint(x: rect.maxX, y: rect.maxY)
]
// ShapeUp method
let points = rect.points(.left, .center, .bottomRight)
A handy enum that represents either a relative or absolute value. This is used in lots of situations throughout ShapeUp to give flexibility when defining parameters.
When setting a corner radius you might want a fixed value like 20 or you might want a value that's 20% of the maximum so that it will scale proportionally. RelatableValue
gives you both of those options.
let absolute = RelatableValue.absolute(20)
let relative = RelatableValue.relative(0.2)
Later, the value is determined by running the value(using total:)
function. Absolute values will always be the same but any relative values will be calculated. In the case of a corner radius, the total would be the maximum radius that would fit that corner given the length of the two lines and the angle of the corner.
let radius = relatableRadius.value(using: maxRadius)
For ease of use, RelatableValue
conforms to ExpressibleByIntegerLiteral
and ExpressibleByFloatLiteral
. This means in many cases you can omit .absolute()
when writing absolute values.
// Both are the same
let cornerStyle = .rounded(radius: .absolute(5))
let cornerStyle = .rounded(radius: 5)
A animatable line Shape with ends that can extend and a position that can offset perpendicular to its direction.
Text("Hello World")
.alignmentGuide(.bottom) { d in
// moves bottom alignment to text baseline
return d[.firstTextBaseline]
}
.background(
SketchyLines(lines: [
.leading(startExtension: -2, endExtension: 10),
.bottom(startExtension: 5, endExtension: 5, offset: .relative(0.05))
], drawAmount: 1)
.stroke(Color.red)
, alignment: .bottom
)
Extensions for InsettableShape
and View
that create an embossed or debossed effect.
*Xcode 16+, iOS 17+, macOS 14+, watchOS 10+, tvOS 17+
Animate lots of properties in a Shape
using AnimatablePack
instead of nesting AnimatablePair
types
Here is an example of animatableData using AnimatablePair:
struct MyShape: Animatable {
var animatableData: AnimatablePair<CGFloat, AnimatablePair<RelatableValue, Double>> {
get { AnimatablePair(insetAmount, AnimatablePair(cornerRadius, rotation)) }
set {
insetAmount = newValue.first
cornerRadius = newValue.second.first
rotation = newValue.second.second
}
}
}
You can see how it would get quite large once you start adding more than a few properties. Here's how to use AnimatablePack instead:
struct MyShape: Animatable {
var animatableData: AnimatablePack<CGFloat, RelatableValue, Double> {
get { AnimatablePack(insetAmount, cornerRadius, rotation) }
set { (insetAmount, cornerRadius, rotation) = newValue() }
}
}