Skip to content

Commit

Permalink
Merge pull request #6 from mkj-is/feature/resultbuilder
Browse files Browse the repository at this point in the history
Swift 5.4 result builder
  • Loading branch information
mkj-is committed May 6, 2021
2 parents e35c85b + 42292eb commit d4a4e12
Show file tree
Hide file tree
Showing 14 changed files with 136 additions and 86 deletions.
8 changes: 3 additions & 5 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
name: Swift

on:
pull_request:
branches:
- master
- pull_request

jobs:
build:
runs-on: macOS-latest
runs-on: macos-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Build
run: swift build -v
- name: Run tests
Expand Down
11 changes: 7 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.1
// swift-tools-version:5.4

import PackageDescription

Expand All @@ -13,14 +13,17 @@ let package = Package(
products: [
.library(
name: "PathBuilder",
targets: ["PathBuilder"])
targets: ["PathBuilder"]
)
],
targets: [
.target(
name: "PathBuilder",
dependencies: []),
dependencies: []
),
.testTarget(
name: "PathBuilderTests",
dependencies: ["PathBuilder"])
dependencies: ["PathBuilder"]
)
]
)
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,11 @@

# PathBuilder

_Path builder_ is a complete function builder for lifting paths into the declarative SwiftUI world. This function 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.
_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.

## Motivation

Just wanted to learn to implement function builders. And during playing with animated paths in SwiftUI found a perfect place to experiment.

## Requirements

- Xcode 11 or above
- Swift 5.1 or above
- iOS 13, macOS 10.15, watchOS 6.0, tvOS 13.0 or above

## Installation

Using Swift Package Manager in Xcode.
I just wanted to learn to implement result builders. And during playing with animated paths in SwiftUI found a perfect place to experiment.

## Usage

Expand Down Expand Up @@ -57,7 +47,8 @@ There are many basic path components present. You can create a new one by confor
- *Arc* – Adds an arc of a circle to the path, specified with a radius and angles.
- *Close* – Closes and completes a subpath in a path.
- *Curve* – Adds a cubic Bézier curve to the path, with the specified end point and control points.
- *Ellipse* – Adds an ellipse that fits inside the specified rectangle.
- *Oval* – Adds an ellipse that fits inside the specified rectangle.
- *EmptySubpath* – Adds empty subpath, used mainly as a temporary placeholder.
- *Line* – Appends a straight line segment from the current point to the specified point.
- *Lines* – Adds a sequence of connected straight-line segments to the path.
- *Move* – Begins a new subpath at the specified point.
Expand All @@ -70,8 +61,22 @@ There are many basic path components present. You can create a new one by confor
#### Grouping components

- *Loop* – Appends components to path iterating over supplied sequence and building path for each element.
- *Subpath* – Groups and appends another subpath object to the path.
- *Transform* – Groups, transforms and appends another transformed subpath object to the path.
- *Subpath* – Groups and appends another subpath object to the path and optionally transforms it.

## Requirements

For Swift 5.1 to 5.3 use package version 1.1.1.

Otherwise for version 2.0+ use latest tools:

- Xcode 12.5 or above
- Swift 5.4 or above
- iOS 13, macOS 10.15, watchOS 6.0, tvOS 13.0 or above

## Installation

Using Swift Package Manager in Xcode
or by adding to your Package manifest file.

## Contributing

Expand Down
9 changes: 9 additions & 0 deletions Sources/PathBuilder/Array+PathComponent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import SwiftUI

extension Array: PathComponent where Element == PathComponent {
public func add(to path: inout Path) {
for component in self {
component.add(to: &path)
}
}
}
9 changes: 9 additions & 0 deletions Sources/PathBuilder/Path components/EmptySubpath.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import SwiftUI

/// Convenience empty path component.
public struct EmptySubpath: PathComponent {
/// Initializes an empty path component.
public init() {}

public func add(to path: inout Path) {}
}
8 changes: 7 additions & 1 deletion Sources/PathBuilder/Path components/Lines.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ public struct Lines: PathComponent {
private let points: [CGPoint]

/// Initializes path component, which adds a sequence of connected straight-line segments to the path.
/// - Parameter points: An array of values that specify 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.
/// - 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.
public init(between points: [CGPoint]) {
self.points = points
}

/// Initializes path component, which adds a sequence of connected straight-line segments to the path.
/// - 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.
public init(_ points: CGPoint...) {
self.points = points
}

public func add(to path: inout Path) {
path.addLines(points)
}
Expand Down
14 changes: 8 additions & 6 deletions Sources/PathBuilder/Path components/Loop.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import SwiftUI

/// Appends components to path iterating over supplied sequence and building path for each element.
public struct Loop: PathComponent {
private let components: [PathComponent]
public struct Loop<S: Sequence>: PathComponent {
private let sequence: S
private let builder: (S.Element) -> Path

/// Creates path component which appends components to path
/// iterating over supplied sequence and building path for each element.
/// - Parameters:
/// - sequence: Sequence of elements which will be used for building path components.
/// - builder: PathBuilder closure receiving each element and creating path component from them.
public init<S: Sequence>(sequence: S, @PathBuilder _ builder: (S.Element) -> PathComponent) {
self.components = sequence.map(builder)
public init(sequence: S, @PathBuilder _ builder: @escaping (S.Element) -> Path) {
self.sequence = sequence
self.builder = builder
}

public func add(to path: inout Path) {
for component in components {
component.add(to: &path)
for element in sequence {
path.addPath(builder(element))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftUI

/// Adds an ellipse that fits inside the specified rectangle.
public struct Ellipse: PathComponent {
public struct Oval: PathComponent {
private let rect: CGRect

/// Initializes path component, which adds an ellipse that fits inside the specified rectangle.
Expand All @@ -14,4 +14,3 @@ public struct Ellipse: PathComponent {
path.addEllipse(in: rect)
}
}

8 changes: 7 additions & 1 deletion Sources/PathBuilder/Path components/Rect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import SwiftUI
public struct Rect: PathComponent {
private let rects: [CGRect]

/// Initializes path component, which adds a set of rectangular subpaths to the path.
/// Initializes path component, which adds an array of rectangular subpaths to the path.
/// - Parameter rects: An array of rectangles, specified in user space coordinates.
public init(_ rects: CGRect...) {
self.rects = rects
}

/// Initializes path component, which adds an array of rectangular subpaths to the path.
/// - Parameter rects: An array of rectangles, specified in user space coordinates.
public init(_ rects: [CGRect]) {
self.rects = rects
}

public func add(to path: inout Path) {
path.addRects(rects)
Expand Down
31 changes: 24 additions & 7 deletions Sources/PathBuilder/Path components/Subpath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,36 @@ import SwiftUI

/// Appends another path object to the path.
public struct Subpath: PathComponent {
private let component: PathComponent
private let transform: CGAffineTransform
private let path: Path

/// Initializes path component, which appends another path object to the path.
/// - Parameter path: Path to be appended.
public init(path: Path = Path()) {
self.component = path
/// - Parameter transform: An affine transform to apply to the subpath before adding to the path.
public init(transform: CGAffineTransform = .identity, path: Path) {
self.transform = transform
self.path = path
}

public init(@PathBuilder _ builder: () -> PathComponent) {
self.component = builder()

/// Intializes subpath using path builder and then transforming it.
/// - Parameters:
/// - transform: An affine transform to apply to the subpath before adding to the path.
/// - builder: Result builder for creating the path from components.
public init(transform: CGAffineTransform = .identity, @PathBuilder _ builder: () -> Path) {
self.transform = transform
self.path = builder()
}

/// Intializes subpath with shape filling the provided rectangle.
/// - Parameters:
/// - shape: Any SwiftUI Shape which will be converted to subpath.
/// - rect: Rectangle frame which will be filled with the shape.
public init<S: Shape>(shape: S, in rect: CGRect) {
self.path = shape.path(in: rect)
self.transform = .identity
}

public func add(to path: inout Path) {
component.add(to: &path)
path.addPath(self.path, transform: transform)
}
}
18 changes: 0 additions & 18 deletions Sources/PathBuilder/Path components/Transform.swift

This file was deleted.

14 changes: 3 additions & 11 deletions Sources/PathBuilder/Path+PathBuilder.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import SwiftUI

public extension Path {
/// Initializes path using custom attribtue path builder.
init(@PathBuilder _ builder: () -> PathComponent) {
self.init { path in
builder().add(to: &path)
}
}
}

extension Path: PathComponent {
public func add(to path: inout Path) {
path.addPath(self)
/// Initializes path using custom attribute path builder.
init(@PathBuilder _ builder: () -> Path) {
self = builder()
}
}
49 changes: 35 additions & 14 deletions Sources/PathBuilder/PathBuilder.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,50 @@
import SwiftUI

/// A custom parameter attribute that constructs paths from closures.
@_functionBuilder
/// A custom result builder which constructs Paths from closures.
@resultBuilder
public struct PathBuilder {

/// Enables support for 'for..in' loops by combining the
/// results of all iterations into a single result.
public static func buildArray(_ components: [PathComponent]) -> PathComponent {
components
}

/// Required by every result builder to build combined results from
/// statement blocks.
public static func buildBlock(_ components: PathComponent...) -> PathComponent {
return Subpath(path: Path { path in
for component in components {
component.add(to: &path)
}
})
components
}

/// Provides support for “if” statements in multi-statement closures, producing an optional path component that is added only when the condition evaluates to true.
public static func buildIf(_ component: PathComponent?) -> PathComponent {
return component.flatMap { component in
Subpath { component }
} ?? Subpath()
/// Enables support for `if` statements that do not have an `else`.
public static func buildOptional(_ component: PathComponent?) -> PathComponent {
component ?? EmptySubpath()
}

/// With buildEither(second:), enables support for 'if-else' and 'switch'
/// statements by folding conditional results into a single result.
public static func buildEither(first: PathComponent) -> PathComponent {
return first
first
}

/// With buildEither(first:), enables support for 'if-else' and 'switch'
/// statements by folding conditional results into a single result.
public static func buildEither(second: PathComponent) -> PathComponent {
return second
second
}

/// This will be called on the partial result of an 'if
/// #available' block to allow the result builder to erase type
/// information.
public static func buildLimitedAvailability(_ component: PathComponent) -> PathComponent {
component
}

/// This will be called on the partial result from the outermost
/// block statement to produce the final returned result Path.
public static func buildFinalResult(_ component: PathComponent) -> Path {
Path { path in
component.add(to: &path)
}
}
}
5 changes: 3 additions & 2 deletions Tests/PathBuilderTests/PathBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ final class PathBuilderTests: XCTestCase {
Arc(center: .zero, radius: .zero, startAngle: .zero, endAngle: .zero, clockwise: true)
Close()
Curve(to: .zero, control1: .zero, control2: .zero)
Ellipse(in: .zero)
Oval(in: .zero)
Move(to: .zero)
Line(to: .zero)
Lines(between: [.zero, .zero])
QuadCurve(to: .zero, control: .zero)
Rect(.zero)
RelativeArc(center: .zero, radius: .zero, startAngle: .zero, delta: .zero)
RoundedRect(in: .zero, cornerSize: .zero)
Subpath()
Subpath(path: Path())
TangentArc(end1: .zero, end2: .zero, radius: .zero)
EmptySubpath()
}
XCTAssertEqual(path.boundingRect, .zero)
}
Expand Down

0 comments on commit d4a4e12

Please sign in to comment.