├── .github
└── workflows
│ └── swift.yml
├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ └── PathBuilder.xcscheme
├── Documentation
├── PathBuilder.svg
├── SwiftLogo.png
└── SwiftLogo.swift
├── LICENSE.txt
├── Package.swift
├── README.md
├── Sources
└── PathBuilder
│ ├── Array+PathComponent.swift
│ ├── LibraryContent.swift
│ ├── Path components
│ ├── Arc.swift
│ ├── Close.swift
│ ├── Curve.swift
│ ├── EmptySubpath.swift
│ ├── Line.swift
│ ├── Lines.swift
│ ├── Loop.swift
│ ├── Move.swift
│ ├── Oval.swift
│ ├── QuadCurve.swift
│ ├── Rect.swift
│ ├── RelativeArc.swift
│ ├── RoundedRect.swift
│ ├── Subpath.swift
│ └── TangentArc.swift
│ ├── Path+PathBuilder.swift
│ ├── PathBuilder.swift
│ └── PathComponent.swift
└── Tests
└── PathBuilderTests
└── PathBuilderTests.swift
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | name: Swift
2 |
3 | on:
4 | - pull_request
5 |
6 | jobs:
7 | build:
8 | runs-on: macos-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 | - name: Build
12 | run: swift build -v
13 | - name: Run tests
14 | run: swift test -v
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/PathBuilder.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
45 |
46 |
48 |
54 |
55 |
56 |
57 |
58 |
68 |
69 |
75 |
76 |
82 |
83 |
84 |
85 |
87 |
88 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/Documentation/PathBuilder.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Documentation/SwiftLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mkj-is/PathBuilder/3247299dc6c3dc63f3bda1326dc7e08b14566af4/Documentation/SwiftLogo.png
--------------------------------------------------------------------------------
/Documentation/SwiftLogo.swift:
--------------------------------------------------------------------------------
1 | Path {
2 | Move(to: CGPoint(x: 87.15, y: 15))
3 | Curve(to: CGPoint(x: 99.68, y: 68.36), control1: CGPoint(x: 99, y: 31.31), control2: CGPoint(x: 104.42, y: 51.03))
4 | Curve(to: CGPoint(x: 98.13, y: 73.09), control1: CGPoint(x: 99.24, y: 69.99), control2: CGPoint(x: 98.72, y: 71.57))
5 | Curve(to: CGPoint(x: 95.95, y: 71.76), control1: CGPoint(x: 97.56, y: 72.7), control2: CGPoint(x: 96.86, y: 72.28))
6 | Curve(to: CGPoint(x: 39.07, y: 24.85), control1: CGPoint(x: 95.95, y: 71.76), control2: CGPoint(x: 68.53, y: 54.77))
7 | Curve(to: CGPoint(x: 73.61, y: 68.36), control1: CGPoint(x: 38.39, y: 24.18), control2: CGPoint(x: 54.99, y: 48.65))
8 | Curve(to: CGPoint(x: 24.51, y: 30.97), control1: CGPoint(x: 64.8, y: 63.26), control2: CGPoint(x: 40.09, y: 45.25))
9 | Curve(to: CGPoint(x: 31.28, y: 40.15), control1: CGPoint(x: 26.54, y: 34.03), control2: CGPoint(x: 28.58, y: 37.43))
10 | Curve(to: CGPoint(x: 81.73, y: 92.83), control1: CGPoint(x: 44.15, y: 56.81), control2: CGPoint(x: 61.42, y: 77.2))
11 | Curve(to: CGPoint(x: 81.76, y: 92.86), control1: CGPoint(x: 81.74, y: 92.84), control2: CGPoint(x: 81.75, y: 92.85))
12 | Curve(to: CGPoint(x: 26.88, y: 92.83), control1: CGPoint(x: 67.47, y: 101.73), control2: CGPoint(x: 47.18, y: 102.38))
13 | Curve(to: CGPoint(x: 13, y: 84.34), control1: CGPoint(x: 21.8, y: 90.45), control2: CGPoint(x: 17.06, y: 87.73))
14 | Curve(to: CGPoint(x: 50.58, y: 116.62), control1: CGPoint(x: 21.46, y: 97.93), control2: CGPoint(x: 34.67, y: 109.83))
15 | Curve(to: CGPoint(x: 103.98, y: 115.99), control1: CGPoint(x: 70.24, y: 125.08), control2: CGPoint(x: 89.77, y: 124.23))
16 | Curve(to: CGPoint(x: 131.84, y: 123.09), control1: CGPoint(x: 110.9, y: 113.07), control2: CGPoint(x: 124.5, y: 108.66))
17 | Curve(to: CGPoint(x: 133.62, y: 117.78), control1: CGPoint(x: 132.55, y: 124.51), control2: CGPoint(x: 133.61, y: 122.19))
18 | Curve(to: CGPoint(x: 123.72, y: 91.48), control1: CGPoint(x: 133.6, y: 111.47), control2: CGPoint(x: 131.55, y: 101.3))
19 | Curve(to: CGPoint(x: 123.48, y: 91.18), control1: CGPoint(x: 123.64, y: 91.38), control2: CGPoint(x: 123.56, y: 91.28))
20 | Curve(to: CGPoint(x: 124.06, y: 89.1), control1: CGPoint(x: 123.69, y: 90.5), control2: CGPoint(x: 123.88, y: 89.8))
21 | Curve(to: CGPoint(x: 87.15, y: 15), control1: CGPoint(x: 130.83, y: 62.92), control2: CGPoint(x: 114.57, y: 31.65))
22 | }
23 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Matěj Kašpar Jirásek
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.4
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "PathBuilder",
7 | platforms: [
8 | .iOS(.v13),
9 | .macOS(.v10_15),
10 | .watchOS(.v6),
11 | .tvOS(.v13)
12 | ],
13 | products: [
14 | .library(
15 | name: "PathBuilder",
16 | targets: ["PathBuilder"]
17 | )
18 | ],
19 | targets: [
20 | .target(
21 | name: "PathBuilder",
22 | dependencies: []
23 | ),
24 | .testTarget(
25 | name: "PathBuilderTests",
26 | dependencies: ["PathBuilder"]
27 | )
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # PathBuilder
4 |
5 | _Path builder_ is a complete result builder for lifting `Path` into the declarative SwiftUI world. This `@resultBuilder` can be used for elegant and short definition of paths. Missing documentation gaps in SwiftUI are filled in using the old but good CGMutablePath knowledge.
6 |
7 | ## Motivation
8 |
9 | I just wanted to learn to implement result builders. And during playing with animated paths in SwiftUI found a perfect place to experiment.
10 |
11 | ## Usage
12 |
13 | ### Examples
14 |
15 | With _PathBuilder_ you can write this to draw a triangle:
16 |
17 | ```swift
18 | Path {
19 | Move(to: CGPoint(x: 50, y: 50))
20 | Line(to: CGPoint(x: 100, y: 100))
21 | Line(to: CGPoint(x: 0, y: 100))
22 | Close()
23 | }
24 | ```
25 |
26 | Instead of longer version:
27 |
28 | ```swift
29 | Path { p in
30 | p.move(to: CGPoint(x: 50, y: 50))
31 | p.addLine(to: CGPoint(x: 100, y: 100))
32 | p.addLine(to: CGPoint(x: 0, y: 100))
33 | p.closeSubpath()
34 | }
35 | ```
36 |
37 | Drawing a Swift logo can be implemented like [this](Documentation/SwiftLogo.swift).
38 |
39 | 
40 |
41 | ### Path components
42 |
43 | There are many basic path components present. You can create a new one by conforming to the `PathComponent` protocol.
44 |
45 | #### Elementary components
46 |
47 | - *Arc* – Adds an arc of a circle to the path, specified with a radius and angles.
48 | - *Close* – Closes and completes a subpath in a path.
49 | - *Curve* – Adds a cubic Bézier curve to the path, with the specified end point and control points.
50 | - *Oval* – Adds an ellipse that fits inside the specified rectangle.
51 | - *EmptySubpath* – Adds empty subpath, used mainly as a temporary placeholder.
52 | - *Line* – Appends a straight line segment from the current point to the specified point.
53 | - *Lines* – Adds a sequence of connected straight-line segments to the path.
54 | - *Move* – Begins a new subpath at the specified point.
55 | - *QuadCurve* – Adds a quadratic Bézier curve to the path, with the specified end point and control point.
56 | - *Rect* – Adds a set of rectangular subpaths to the path.
57 | - *RelativeArc* – Adds an arc of a circle to the path, specified with a radius and a difference in angle.
58 | - *RoundedRect* – Adds a subpath to the path, in the shape of a rectangle with rounded corners.
59 | - *TangentArc* – Adds an arc of a circle to the path, specified with a radius and two tangent lines.
60 |
61 | #### Grouping components
62 |
63 | - *Loop* – Appends components to path iterating over supplied sequence and building path for each element.
64 | - *Subpath* – Groups and appends another subpath object to the path and optionally transforms it.
65 |
66 | ## Requirements
67 |
68 | For Swift 5.1 to 5.3 use package version 1.1.1.
69 |
70 | Otherwise for version 2.0+ use latest tools:
71 |
72 | - Xcode 12.5 or above
73 | - Swift 5.4 or above
74 | - iOS 13, macOS 10.15, watchOS 6.0, tvOS 13.0 or above
75 |
76 | ## Installation
77 |
78 | Using Swift Package Manager in Xcode
79 | or by adding to your Package manifest file.
80 |
81 | ## Contributing
82 |
83 | All contributions are welcome.
84 |
85 | Project was created by [Matěj Kašpar Jirásek](https://github.com/mkj-is).
86 |
87 | Project is licensed under [MIT license](LICENSE.txt).
88 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Array+PathComponent.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension Array: PathComponent where Element == PathComponent {
4 | public func add(to path: inout Path) {
5 | for component in self {
6 | component.add(to: &path)
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/LibraryContent.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import DeveloperToolsSupport
3 |
4 | @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *)
5 | struct LibraryContent: LibraryContentProvider {
6 | var views: [LibraryItem] {
7 | LibraryItem(
8 | Path {
9 | Move(to: CGPoint(x: 0, y: 0))
10 | // ...
11 | Close()
12 | }
13 | )
14 | LibraryItem(
15 | Subpath {
16 | Move(to: CGPoint(x: 0, y: 0))
17 | // ...
18 | }
19 | )
20 | LibraryItem(Arc(center: .zero, radius: 20, startAngle: .zero, endAngle: .degrees(180)))
21 | LibraryItem(Close())
22 | LibraryItem(Curve(to: CGPoint(x: 10, y: 10), control1: CGPoint(x: 5, y: 5), control2: CGPoint(x: 10, y: 10)))
23 | LibraryItem(EmptySubpath())
24 | LibraryItem(Line(to: CGPoint(x: 50, y: 50)))
25 | LibraryItem(Lines(CGPoint(x: 60, y: 60), CGPoint(x: 70, y: 70)))
26 | LibraryItem(Oval(in: CGRect(x: 0, y: 0, width: 10, height: 20)))
27 | LibraryItem(QuadCurve(to: CGPoint(x: 0, y: 0), control: CGPoint(x: 10, y: 10)))
28 | LibraryItem(Rect(CGRect(x: 0, y: 0, width: 100, height: 100), CGRect(x: 10, y: 10, width: 20, height: 30)))
29 | LibraryItem(RelativeArc(center: CGPoint(x: 0, y: 0), radius: 10, startAngle: .degrees(60), delta: .degrees(180)))
30 | LibraryItem(RoundedRect(in: CGRect(x: 10, y: 10, width: 20, height: 30), cornerSize: CGSize(width: 5, height: 10)))
31 | LibraryItem(TangentArc(end1: CGPoint(x: 0, y: 0), end2: CGPoint(x: 10, y: 10), radius: 3))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/Arc.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Adds an arc of a circle to the path, specified with a radius and angles.
4 | public struct Arc: PathComponent {
5 |
6 | private let center: CGPoint
7 | private let radius: CGFloat
8 | private let startAngle, endAngle: Angle
9 | private let clockwise: Bool
10 |
11 | /// Initializes path component, which adds an arc of a circle to the path, specified with a radius and angles.
12 | /// - Parameter center: The center of the arc, in user space coordinates.
13 | /// - Parameter radius: The radius of the arc, in user space coordinates.
14 | /// - Parameter startAngle: The angle to the starting point of the arc from the positive x-axis.
15 | /// - Parameter endAngle: The angle to the end point of the arc from the positive x-axis.
16 | /// - Parameter clockwise: `true` to make a clockwise arc; `false` to make a counterclockwise arc.
17 | public init(center: CGPoint, radius: CGFloat, startAngle: Angle, endAngle: Angle, clockwise: Bool = true) {
18 | self.center = center
19 | self.radius = radius
20 | self.startAngle = startAngle
21 | self.endAngle = endAngle
22 | self.clockwise = clockwise
23 | }
24 |
25 | public func add(to path: inout Path) {
26 | path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/Close.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Closes and completes a subpath in a path.
4 | public struct Close: PathComponent {
5 | /// Initializes path component, which closes and completes a subpath in a path.
6 | public init() {}
7 |
8 | public func add(to path: inout Path) {
9 | path.closeSubpath()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/Curve.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Adds a cubic Bézier curve to the path, with the specified end point and control points.
4 | public struct Curve: PathComponent {
5 | private let point, control1, control2: CGPoint
6 |
7 | /// Initializes path component, which adds a cubic Bézier curve to the path, with the specified end point and control points.
8 | /// - Parameter point: The point, in user space coordinates, at which to end the curve.
9 | /// - Parameter control1: The first control point of the curve, in user space coordinates.
10 | /// - Parameter control2: The second control point of the curve, in user space coordinates.
11 | public init(to point: CGPoint, control1: CGPoint, control2: CGPoint) {
12 | self.point = point
13 | self.control1 = control1
14 | self.control2 = control2
15 | }
16 |
17 | public func add(to path: inout Path) {
18 | path.addCurve(to: point, control1: control1, control2: control2)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/EmptySubpath.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Convenience empty path component.
4 | public struct EmptySubpath: PathComponent {
5 | /// Initializes an empty path component.
6 | public init() {}
7 |
8 | public func add(to path: inout Path) {}
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/Line.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Appends a straight line segment from the current point to the specified point.
4 | public struct Line: PathComponent {
5 | private let point: CGPoint
6 |
7 | /// Initializes path component, which appends a straight line segment from the current point to the specified point.
8 | /// - Parameter point: The location, in user space coordinates, for the end of the new line segment.
9 | public init(to point: CGPoint) {
10 | self.point = point
11 | }
12 |
13 | public func add(to path: inout Path) {
14 | path.addLine(to: point)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/Lines.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Adds a sequence of connected straight-line segments to the path.
4 | public struct Lines: PathComponent {
5 | private let points: [CGPoint]
6 |
7 | /// Initializes path component, which adds a sequence of connected straight-line segments to the path.
8 | /// - Parameter points: An array of values which specifies the start and end points of the line segments to draw. Each point in the array specifies a position in user space. The first point in the array specifies the initial starting point.
9 | public init(between points: [CGPoint]) {
10 | self.points = points
11 | }
12 |
13 | /// Initializes path component, which adds a sequence of connected straight-line segments to the path.
14 | /// - Parameter points: Variable arguments which specifies the start and end points of the line segments to draw. Each point in the array specifies a position in user space. The first point in the array specifies the initial starting point.
15 | public init(_ points: CGPoint...) {
16 | self.points = points
17 | }
18 |
19 | public func add(to path: inout Path) {
20 | path.addLines(points)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/Loop.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Appends components to path iterating over supplied sequence and building path for each element.
4 | public struct Loop: PathComponent {
5 | private let sequence: S
6 | private let builder: (S.Element) -> PathComponent
7 |
8 | /// Creates path component which appends components to path
9 | /// iterating over supplied sequence and building path for each element.
10 | /// - Parameters:
11 | /// - sequence: Sequence of elements which will be used for building path components.
12 | /// - builder: PathBuilder closure receiving each element and creating path component from them.
13 | public init(sequence: S, @PathBuilder _ builder: @escaping (S.Element) -> PathComponent) {
14 | self.sequence = sequence
15 | self.builder = builder
16 | }
17 |
18 | public func add(to path: inout Path) {
19 | for element in sequence {
20 | builder(element).add(to: &path)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/Move.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Begins a new subpath at the specified point.
4 | public struct Move: PathComponent {
5 | private let point: CGPoint
6 |
7 | /// Initializes path component, which begins a new subpath at the specified point.
8 | /// - Parameter point: The point, in user space coordinates, at which to start a new subpath.
9 | public init(to point: CGPoint) {
10 | self.point = point
11 | }
12 |
13 | public func add(to path: inout Path) {
14 | path.move(to: point)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/Oval.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Adds an ellipse that fits inside the specified rectangle.
4 | public struct Oval: PathComponent {
5 | private let rect: CGRect
6 |
7 | /// Initializes path component, which adds an ellipse that fits inside the specified rectangle.
8 | /// - Parameter rect: A rectangle that defines the area for the ellipse to fit in.
9 | public init(in rect: CGRect) {
10 | self.rect = rect
11 | }
12 |
13 | public func add(to path: inout Path) {
14 | path.addEllipse(in: rect)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/QuadCurve.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Adds a quadratic Bézier curve to the path, with the specified end point and control point.
4 | public struct QuadCurve: PathComponent {
5 | private let point, control: CGPoint
6 |
7 | /// Initializes path component, which adds a quadratic Bézier curve to the path, with the specified end point and control point.
8 | /// - Parameter point: The point, in user space coordinates, at which to end the curve.
9 | /// - Parameter control: The control point of the curve, in user space coordinates.
10 | public init(to point: CGPoint, control: CGPoint) {
11 | self.point = point
12 | self.control = control
13 | }
14 |
15 | public func add(to path: inout Path) {
16 | path.addQuadCurve(to: point, control: control)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/Rect.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Adds a set of rectangular subpaths to the path.
4 | public struct Rect: PathComponent {
5 | private let rects: [CGRect]
6 |
7 | /// Initializes path component, which adds an array of rectangular subpaths to the path.
8 | /// - Parameter rects: An array of rectangles, specified in user space coordinates.
9 | public init(_ rects: CGRect...) {
10 | self.rects = rects
11 | }
12 |
13 | /// Initializes path component, which adds an array of rectangular subpaths to the path.
14 | /// - Parameter rects: An array of rectangles, specified in user space coordinates.
15 | public init(_ rects: [CGRect]) {
16 | self.rects = rects
17 | }
18 |
19 | public func add(to path: inout Path) {
20 | path.addRects(rects)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/RelativeArc.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Adds an arc of a circle to the path, specified with a radius and a difference in angle.
4 | public struct RelativeArc: PathComponent {
5 | private let center: CGPoint
6 | private let radius: CGFloat
7 | private let startAngle, delta: Angle
8 |
9 | /// Initializes path component, which adds an arc of a circle to the path, specified with a radius and a difference in angle.
10 | /// - Parameter center: The center of the arc, in user space coordinates.
11 | /// - Parameter radius: The radius of the arc, in user space coordinates.
12 | /// - Parameter startAngle: The angle to the starting point of the arc, measured from the positive x-axis.
13 | /// - Parameter delta: The difference between the starting angle and ending angle of the arc. A positive value creates a counter-clockwise arc (in user space coordinates), and vice versa.
14 | public init(center: CGPoint, radius: CGFloat, startAngle: Angle, delta: Angle) {
15 | self.center = center
16 | self.radius = radius
17 | self.startAngle = startAngle
18 | self.delta = delta
19 | }
20 |
21 | public func add(to path: inout Path) {
22 | path.addRelativeArc(center: center, radius: radius, startAngle: startAngle, delta: delta)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/RoundedRect.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Adds a subpath to the path, in the shape of a rectangle with rounded corners.
4 | public struct RoundedRect: PathComponent {
5 | private let rect: CGRect
6 | private let cornerSize: CGSize
7 | private let style: RoundedCornerStyle
8 |
9 | /// Initializes path component, which adds a subpath to the path, in the shape of a rectangle with rounded corners.
10 | /// - Parameter rect: The rectangle to add, specified in user space coordinates.
11 | /// - Parameter cornerSize: The size, in user space coordinates, for rounded corner sections.
12 | /// - Parameter style: The shape of a rounded rectangle's corners.
13 | public init(in rect: CGRect, cornerSize: CGSize, style: RoundedCornerStyle = .circular) {
14 | self.rect = rect
15 | self.cornerSize = cornerSize
16 | self.style = style
17 | }
18 |
19 | public func add(to path: inout Path) {
20 | path.addRoundedRect(in: rect, cornerSize: cornerSize, style: style)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/Subpath.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Appends another path object to the path.
4 | public struct Subpath: PathComponent {
5 | private let transform: CGAffineTransform
6 | private let path: Path
7 |
8 | /// Initializes path component, which appends another path object to the path.
9 | /// - Parameter path: Path to be appended.
10 | /// - Parameter transform: An affine transform to apply to the subpath before adding to the path.
11 | public init(transform: CGAffineTransform = .identity, path: Path) {
12 | self.transform = transform
13 | self.path = path
14 | }
15 |
16 | /// Intializes subpath using path builder and then transforming it.
17 | /// - Parameters:
18 | /// - transform: An affine transform to apply to the subpath before adding to the path.
19 | /// - builder: Result builder for creating the path from components.
20 | public init(transform: CGAffineTransform = .identity, @PathBuilder _ builder: () -> PathComponent) {
21 | self.transform = transform
22 | self.path = Path(builder)
23 | }
24 |
25 | /// Intializes subpath with shape filling the provided rectangle.
26 | /// - Parameters:
27 | /// - shape: Any SwiftUI Shape which will be converted to subpath.
28 | /// - rect: Rectangle frame which will be filled with the shape.
29 | public init(shape: S, in rect: CGRect) {
30 | self.path = shape.path(in: rect)
31 | self.transform = .identity
32 | }
33 |
34 | public func add(to path: inout Path) {
35 | path.addPath(self.path, transform: transform)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path components/TangentArc.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Adds an arc of a circle to the path, specified with a radius and two tangent lines.
4 | public struct TangentArc: PathComponent {
5 |
6 | private let end1, end2: CGPoint
7 | private let radius: CGFloat
8 |
9 | /// Initializes path component, which adds an arc of a circle to the path, specified with a radius and two tangent lines.
10 | /// - Parameter end1: The end point, in user space coordinates, for the first tangent line to be used in constructing the arc. (The start point for this tangent line is the path's current point.)
11 | /// - Parameter end2: The end point, in user space coordinates, for the second tangent line to be used in constructing the arc. (The start point for this tangent line is the tangent1End point.)
12 | /// - Parameter radius: The radius of the arc, in user space coordinates.
13 | public init(end1: CGPoint, end2: CGPoint, radius: CGFloat) {
14 | self.end1 = end1
15 | self.end2 = end2
16 | self.radius = radius
17 | }
18 |
19 | public func add(to path: inout Path) {
20 | path.addArc(tangent1End: end1, tangent2End: end2, radius: radius)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/Path+PathBuilder.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension Path {
4 | /// Initializes path using custom attribute path builder.
5 | init(@PathBuilder _ builder: () -> PathComponent) {
6 | self = Path(builder().add(to:))
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/PathBuilder.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// A custom result builder which constructs Paths from closures.
4 | @resultBuilder
5 | public struct PathBuilder {
6 |
7 | /// Enables support for 'for..in' loops by combining the
8 | /// results of all iterations into a single result.
9 | public static func buildArray(_ components: [PathComponent]) -> PathComponent {
10 | components
11 | }
12 |
13 | /// Required by every result builder to build combined results from
14 | /// statement blocks.
15 | public static func buildBlock(_ components: PathComponent...) -> PathComponent {
16 | components
17 | }
18 |
19 | /// Enables support for `if` statements that do not have an `else`.
20 | public static func buildOptional(_ component: PathComponent?) -> PathComponent {
21 | component ?? EmptySubpath()
22 | }
23 |
24 | /// With `buildEither(second:)`, enables support for 'if-else' and 'switch'
25 | /// statements by folding conditional results into a single result.
26 | public static func buildEither(first: PathComponent) -> PathComponent {
27 | first
28 | }
29 |
30 | /// With `buildEither(first:)`, enables support for 'if-else' and 'switch'
31 | /// statements by folding conditional results into a single result.
32 | public static func buildEither(second: PathComponent) -> PathComponent {
33 | second
34 | }
35 |
36 | /// This will be called on the partial result of an 'if #available'
37 | /// block to allow the result builder to erase type information.
38 | public static func buildLimitedAvailability(_ component: PathComponent) -> PathComponent {
39 | component
40 | }
41 |
42 | /// This will be called on the final result from the outermost
43 | /// block statement to produce the partial `PathComponent`.
44 | public static func buildFinalResult(_ component: PathComponent) -> PathComponent {
45 | component
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/PathBuilder/PathComponent.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// A component, which can be added to a path.
4 | public protocol PathComponent {
5 | /// Adds itself to the path.
6 | func add(to path: inout Path)
7 | }
8 |
--------------------------------------------------------------------------------
/Tests/PathBuilderTests/PathBuilderTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import PathBuilder
3 | import SwiftUI
4 |
5 | final class PathBuilderTests: XCTestCase {
6 |
7 | func testAllComponents() {
8 | let path = Path {
9 | Arc(center: .zero, radius: .zero, startAngle: .zero, endAngle: .zero, clockwise: true)
10 | Close()
11 | Curve(to: .zero, control1: .zero, control2: .zero)
12 | Oval(in: .zero)
13 | Move(to: .zero)
14 | Line(to: .zero)
15 | Lines(between: [.zero, .zero])
16 | QuadCurve(to: .zero, control: .zero)
17 | Rect(.zero)
18 | RelativeArc(center: .zero, radius: .zero, startAngle: .zero, delta: .zero)
19 | RoundedRect(in: .zero, cornerSize: .zero)
20 | Subpath(path: Path())
21 | TangentArc(end1: .zero, end2: .zero, radius: .zero)
22 | EmptySubpath()
23 | }
24 | XCTAssertEqual(path.boundingRect, .zero)
25 | }
26 |
27 | func testSubpaths() {
28 | let path = Path {
29 | Subpath {
30 | Move(to: CGPoint(x: 100, y: 100))
31 | Line(to: .zero)
32 | }
33 | Subpath {
34 | Move(to: CGPoint(x: 100, y: 0))
35 | Line(to: CGPoint(x: 0, y: 100))
36 | }
37 | Subpath {
38 | Move(to: CGPoint(x: 0, y: 100))
39 | Line(to: CGPoint(x: 100, y: 0))
40 | }
41 | }
42 | XCTAssertEqual(CGRect(origin: .zero, size: CGSize(width: 100, height: 100)), path.boundingRect)
43 | }
44 |
45 | func testLoop() {
46 | let path = Path {
47 | Loop(sequence: 0...100) { i in
48 | Move(to: .zero)
49 | Line(to: CGPoint(x: sin(CGFloat(i)) * 100, y: cos(CGFloat(i)) * 100))
50 | }
51 | }
52 | XCTAssertNotEqual(path.boundingRect, .zero)
53 | }
54 |
55 | func testSingleComponentPath() {
56 | let path = Path {
57 | Move(to: .zero)
58 | }
59 | XCTAssertEqual(path.boundingRect.size, .zero)
60 | }
61 |
62 | func testIfPath() {
63 | let draw = true
64 | let path = Path {
65 | if draw {
66 | Rect(CGRect(origin: .zero, size: CGSize(width: 100, height: 100)))
67 | }
68 | }
69 | XCTAssertEqual(path.boundingRect, CGRect(origin: .zero, size: CGSize(width: 100, height: 100)))
70 | }
71 |
72 | func testIfNotPath() {
73 | let draw = false
74 | let path = Path {
75 | Move(to: .zero)
76 | if draw {
77 | Line(to: CGPoint(x: 100, y: 100))
78 | }
79 | Close()
80 | }
81 | XCTAssertEqual(path.boundingRect, .zero)
82 | }
83 |
84 | func testEitherPath() {
85 | let draw = true
86 | let path = Path {
87 | if draw {
88 | Rect(CGRect(origin: .zero, size: CGSize(width: 100, height: 100)))
89 | } else {
90 | Rect(CGRect(origin: .zero, size: CGSize(width: 50, height: 50)))
91 | }
92 | }
93 | XCTAssertEqual(path.boundingRect, CGRect(origin: .zero, size: CGSize(width: 100, height: 100)))
94 | }
95 |
96 | func testElsePath() {
97 | let draw = false
98 | let path = Path {
99 | if draw {
100 | Rect(CGRect(origin: .zero, size: CGSize(width: 100, height: 100)))
101 | } else {
102 | Rect(CGRect(origin: .zero, size: CGSize(width: 50, height: 50)))
103 | }
104 | }
105 | XCTAssertEqual(path.boundingRect, CGRect(origin: .zero, size: CGSize(width: 50, height: 50)))
106 | }
107 | }
108 |
--------------------------------------------------------------------------------