PureSwiftUIDesign is a Swift package that brings joy to the process of creating designs using paths in SwiftUI code.

Motivation

Creating paths in SwiftUI can be a bit of a pain. This is largely due to the calculation and creation of points that litter the resulting code not only making complex designs lengthy to write, but next to impossible to decode what's actually going on when you're reading it.

PureSwiftUIDesign allows you to create incredibly complex and even animated designs quickly, while keeping the code simple.

It is my hope that the ease with which you can construct shapes using PureSwiftUIDesign's layout guides and Path extensions will encourage people to explore their artistic capabilities with constructing paths rather than be turned off by the ubiquitous point calculation logic that appears in most path building example code. Without these hurdles, you really are limited only by you imagination.

There are two main aspects to the package: layout Guides and a multitude of extensions all designed to support the act of drawing shapes.

Layout Guides

Layout Guides offer a new way to draw shapes in SwiftUI. In short, they completely remove the need to calculate points, thereby avoiding the tortuous and lengthy process of declaring them throughout your path drawing code.

In fact, the original logo for PureSwiftUIDesign was created using layout guides, including the visibility of the control points in the curves used in the design, the gist of which can be found here.

For something a bit similar, consider the following code that draws six pointed star:

struct StarShapeNative: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            let numSegments = 12
            let outerRadius = min(rect.height, rect.width) / 2
            let innerRadius = outerRadius * innerRadiusRatio
            let center = CGPoint(x: rect.midX, y: rect.midY)
            let stepAngle = 2 * .pi / CGFloat(numSegments)
            
            path.move(to: CGPoint(x: rect.midX, y: rect.minY))
            
            for index in 0..<numSegments {
                
                let angle = CGFloat(index) * stepAngle
                let radius = index.isMultiple(of: 2) ? outerRadius : innerRadius
                let xOffset = radius * sin(angle)
                let yOffset = radius * cos(angle)
                
                path.addLine(to: CGPoint(x: center.x + xOffset, y: center.y - yOffset))
            }
            path.closeSubpath()
        }
    }
}

Note the verbosity on display in that code, including the usage of trigonometry to calculate the points. The point calculation really gets in the way of understanding what's being created meaning it's difficult to reason about the code.

Contrast this with creating the same star using layout guides:

private let starLayoutConfig = LayoutGuideConfig.polar(rings: [innerRadiusRatio, 1], segments: 12)

struct StarShape: Shape {
    
    func path(in rect: CGRect) -> Path {
        Path { path in
            let g = starLayoutConfig.layout(in: rect)
            
            path.move(g[1, 0]) // move to polar coordinate [1, 0] denoting the ring and segment
            
            for segment in 1..<g.yCount {
                // draw a line to the coordinate of the outer ring if the segment is even, otherwise go to the inner ring
                path.line(g[segment.isOdd ? 0 : 1, segment])
            }
            path.closeSubpath()
        }
    }
}

The takeaway is that we are declaratively saying what we want to do, not how to do it. We don't care about the location of the points themselves, we just ask for the point at a particular coordinate and let the framework sort it out. No trignometry is required and therefore we don't have any calculations cluttering up the intent of the code.

For a detailed explanation of the differences and advantages, please watch this video where I cover creating that star using polar layout guides:

Polar Layout Guides

There is a whole lot more to layout guides, including the ability to transform and animate the guides themselves. Anything you can imagine, you can do with ease using these constructs. Read the documentation for all the details.

Extensions

PureSwiftUIDesign includes a multitude of extensions to make the process of creating paths a succinct and enjoyable one. Once you explore all that this framework has to offer, shape construction becomes more like building something out of lego.

Read about all the available extensions and utilities here.

Caveats

PureSwiftUIDesign defines angles as starting from the trailing edge (as per native SwiftUI) and increasing in a clockwise direction. Layout guides on the other hand, specifically of type polar, define the starting angle, by default, at the top. This makes sense for polar design work since if there is a reflective symmetry to the design, it is more likely to be on the vertical axis. Also, designs are easier to reason about if the angle starts from the top. You can override this behaviour by setting fromTop to false like so:

let layoutConfig = LayoutGuideConfig.polar(rings: 2, segments: 8, fromTop: false)

Be warned, however. The predefined constants in Angle such as leading and trailing are defined as per the native SwiftUI way of doing things. So if you want to specify angles like this:

let layoutConfig = LayoutGuideConfig.polar(rings: 2, segments: [.trailing, .bottom, .leading], fromTop: false)

you must remember to set fromTop to false or the resulting angles will be offset 90 degrees anti-clockwise.

Installation

The pure-swift-ui-design package can be found at:

https://github.com/CodeSlicing/pure-swift-ui-design.git

Instructions for installing swift packages can be found here.

Versioning

This project adheres to a semantic versioning paradigm, so breaking changes will be reserved for major version updates.

Version History

  • 1.0.0 Commit initial code

Licensing

This project is licensed under the MIT License - see here for details.

Contact

You can contact me on Twitter @CodeSlice. Happy to hear suggestions for improving the package, mistakes I've made, or feature requests. I won't be open-sourcing the project at this time since I don't have time to administer PRs, I'm afraid.