├── Assets
├── Docs
│ ├── Extensions
│ │ ├── Images
│ │ │ ├── shapes-star.png
│ │ │ ├── rect-center-demo.png
│ │ │ ├── three-rect-demo.png
│ │ │ ├── angle-coords-demo.png
│ │ │ ├── offset-with-angle-demo.png
│ │ │ ├── rect-bottom-right-demo.png
│ │ │ └── shapes-corner-radius-tuples.png
│ │ └── Animations
│ │ │ └── up-to-tick-demo-animation.gif
│ ├── LayoutGuides
│ │ ├── Images
│ │ │ ├── arrow-demo.png
│ │ │ ├── notch-demo.png
│ │ │ ├── polygon-sides-demo.png
│ │ │ ├── heart-drawing-cp-demo.png
│ │ │ ├── heart-drawing-result.png
│ │ │ └── complex-layout-guide-combining-demo.png
│ │ └── Animations
│ │ │ ├── rain-icon-demo.gif
│ │ │ ├── shield-animation.gif
│ │ │ ├── train-wheel-demo.gif
│ │ │ ├── heart-animation-demo.gif
│ │ │ ├── up-to-tick-animation-demo.gif
│ │ │ ├── triangle-animated-all-demo.gif
│ │ │ ├── triangle-animated-rotation-demo.gif
│ │ │ ├── train-wheel-demo-with-layout-guides.gif
│ │ │ └── triangle-animated-all-properties-demo.gif
│ └── LICENCE.md
└── Images
│ ├── pure-swift-ui-design-logo-01.png
│ └── LayoutGuides
│ ├── layout-guides_part-01_thumbnail.png
│ ├── layout-guides_part-02_thumbnail.png
│ ├── layout-guides_part-03_thumbnail.png
│ ├── layout-guides_part-04_thumbnail.png
│ ├── layout-guides_part-05_thumbnail.png
│ ├── layout-guides_part-06_thumbnail.png
│ ├── layout-guides_part-07_thumbnail.png
│ ├── captain-america-shield_part-01_thumbnail.png
│ └── captain-america-shield_part-02_thumbnail.png
├── Sources
└── PureSwiftUIDesign
│ ├── ExportedModules.swift
│ ├── GlobalFunctions
│ ├── TrigonometryFunctions.swift
│ └── GeometryFunctions.swift
│ ├── Extensions
│ └── Convenience
│ │ ├── Foundation
│ │ ├── Float+Convenience.swift
│ │ ├── Double+Convenience.swift
│ │ ├── FloatingPoint+Convenience.swift
│ │ ├── Float+Angle.swift
│ │ ├── Comparable+Convenience.swift
│ │ ├── Int+Convenience.swift
│ │ ├── Double+Angle.swift
│ │ └── Int+Angle.swift
│ │ ├── CoreGraphics
│ │ ├── CGFloat+Convenience.swift
│ │ ├── CGFloat+Angle.swift
│ │ ├── CGAffineTransform+Convenience.swift
│ │ ├── CGVector+Convenience.swift
│ │ ├── CGSize+Convenience.swift
│ │ ├── CGRect+Convenience.swift
│ │ └── CGPoint+Convenience.swift
│ │ ├── SwiftUI
│ │ ├── UnitPoint+Convenience.swift
│ │ ├── GeometryProxy+Convenience.swift
│ │ ├── Path+LinesAndShapes.swift
│ │ ├── Angle+Convenience.swift
│ │ └── Path+Convenience.swift
│ │ └── UIKit
│ │ └── UIScreen+Convenience.swift
│ ├── Internal
│ ├── Convenience
│ │ └── Edge+Convenience.swift
│ └── Modifiers
│ │ └── View+InternalModifiers.swift
│ ├── Protocols
│ └── Types
│ │ ├── RepresentableAsInt.swift
│ │ ├── RepresentableAsFloat.swift
│ │ ├── RepresentableAsDouble.swift
│ │ └── RepresentableAsCGFloat.swift
│ └── Model
│ └── LayoutGuide
│ ├── LayoutGuideEnvironment.swift
│ ├── View+LayoutGuide.swift
│ ├── LayoutCoordinator.swift
│ ├── GridLayoutCoordinator.swift
│ ├── LayoutGuide.swift
│ ├── LayoutGuideConfig.swift
│ └── PolarLayoutCoordinator.swift
├── Tests
└── PureSwiftUIDesignTests
│ ├── Model
│ └── LayoutGuide
│ │ ├── BaseLayoutGuideTests.swift
│ │ └── LayoutCoordinatorTests.swift
│ ├── GlobalFunctions
│ ├── TrigonometryFunctionsTests.swift
│ └── GeometryFunctionsTests.swift
│ ├── Extensions
│ └── Convenience
│ │ ├── SwiftUI
│ │ ├── Edge+ConvenienceTests.swift
│ │ ├── UnitPoint+ConvenienceTests.swift
│ │ └── Angle+ConvenienceTests.swift
│ │ ├── Foundation
│ │ ├── Double+ConvenienceTests.swift
│ │ ├── FloatingPoint+ConvenienceTests.swift
│ │ ├── Float+ConvenienceTests.swift
│ │ ├── Comparable+Tests.swift
│ │ ├── Float+AngleTests.swift
│ │ ├── Double+AngleTests.swift
│ │ ├── Int+ConvenienceTests.swift
│ │ └── Int+AngleTests.swift
│ │ ├── CoreGraphics
│ │ ├── CGFloat+ConvenienceTests.swift
│ │ ├── CGAffineTransform+ConvenienceTests.swift
│ │ ├── CGFloat+AngleTests.swift
│ │ ├── CGVector+ConvenienceTests.swift
│ │ ├── CGSize+ConvenienceTests.swift
│ │ └── CGRect+ConvenienceTests.swift
│ │ └── UIKit
│ │ └── UIScreentConvenience+Tests.swift
│ └── Util
│ └── TestFunctions.swift
├── Package.swift
├── PureSwiftUIDesign.podspec
├── .gitignore
└── README.md
/Assets/Docs/Extensions/Images/shapes-star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/Extensions/Images/shapes-star.png
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Images/arrow-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Images/arrow-demo.png
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Images/notch-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Images/notch-demo.png
--------------------------------------------------------------------------------
/Assets/Images/pure-swift-ui-design-logo-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Images/pure-swift-ui-design-logo-01.png
--------------------------------------------------------------------------------
/Assets/Docs/Extensions/Images/rect-center-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/Extensions/Images/rect-center-demo.png
--------------------------------------------------------------------------------
/Assets/Docs/Extensions/Images/three-rect-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/Extensions/Images/three-rect-demo.png
--------------------------------------------------------------------------------
/Assets/Docs/Extensions/Images/angle-coords-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/Extensions/Images/angle-coords-demo.png
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Animations/rain-icon-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Animations/rain-icon-demo.gif
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Images/polygon-sides-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Images/polygon-sides-demo.png
--------------------------------------------------------------------------------
/Assets/Docs/Extensions/Images/offset-with-angle-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/Extensions/Images/offset-with-angle-demo.png
--------------------------------------------------------------------------------
/Assets/Docs/Extensions/Images/rect-bottom-right-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/Extensions/Images/rect-bottom-right-demo.png
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Animations/shield-animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Animations/shield-animation.gif
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Animations/train-wheel-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Animations/train-wheel-demo.gif
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Images/heart-drawing-cp-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Images/heart-drawing-cp-demo.png
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Images/heart-drawing-result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Images/heart-drawing-result.png
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Animations/heart-animation-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Animations/heart-animation-demo.gif
--------------------------------------------------------------------------------
/Assets/Docs/Extensions/Images/shapes-corner-radius-tuples.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/Extensions/Images/shapes-corner-radius-tuples.png
--------------------------------------------------------------------------------
/Assets/Images/LayoutGuides/layout-guides_part-01_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Images/LayoutGuides/layout-guides_part-01_thumbnail.png
--------------------------------------------------------------------------------
/Assets/Images/LayoutGuides/layout-guides_part-02_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Images/LayoutGuides/layout-guides_part-02_thumbnail.png
--------------------------------------------------------------------------------
/Assets/Images/LayoutGuides/layout-guides_part-03_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Images/LayoutGuides/layout-guides_part-03_thumbnail.png
--------------------------------------------------------------------------------
/Assets/Images/LayoutGuides/layout-guides_part-04_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Images/LayoutGuides/layout-guides_part-04_thumbnail.png
--------------------------------------------------------------------------------
/Assets/Images/LayoutGuides/layout-guides_part-05_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Images/LayoutGuides/layout-guides_part-05_thumbnail.png
--------------------------------------------------------------------------------
/Assets/Images/LayoutGuides/layout-guides_part-06_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Images/LayoutGuides/layout-guides_part-06_thumbnail.png
--------------------------------------------------------------------------------
/Assets/Images/LayoutGuides/layout-guides_part-07_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Images/LayoutGuides/layout-guides_part-07_thumbnail.png
--------------------------------------------------------------------------------
/Assets/Docs/Extensions/Animations/up-to-tick-demo-animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/Extensions/Animations/up-to-tick-demo-animation.gif
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Animations/up-to-tick-animation-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Animations/up-to-tick-animation-demo.gif
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Animations/triangle-animated-all-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Animations/triangle-animated-all-demo.gif
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Animations/triangle-animated-rotation-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Animations/triangle-animated-rotation-demo.gif
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Images/complex-layout-guide-combining-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Images/complex-layout-guide-combining-demo.png
--------------------------------------------------------------------------------
/Assets/Images/LayoutGuides/captain-america-shield_part-01_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Images/LayoutGuides/captain-america-shield_part-01_thumbnail.png
--------------------------------------------------------------------------------
/Assets/Images/LayoutGuides/captain-america-shield_part-02_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Images/LayoutGuides/captain-america-shield_part-02_thumbnail.png
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Animations/train-wheel-demo-with-layout-guides.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Animations/train-wheel-demo-with-layout-guides.gif
--------------------------------------------------------------------------------
/Assets/Docs/LayoutGuides/Animations/triangle-animated-all-properties-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeSlicing/pure-swift-ui-design/HEAD/Assets/Docs/LayoutGuides/Animations/triangle-animated-all-properties-demo.gif
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/ExportedModules.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExportedModules.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 20/11/2019.
6 | // Copyright © 2019 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | @_exported import SwiftUI
10 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/GlobalFunctions/TrigonometryFunctions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TrigonometryFunctions.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 20/11/2019.
6 | // Copyright © 2019 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public func cos(_ angle: Angle) -> Double {
12 | cos(angle.radians)
13 | }
14 |
15 | public func sin(_ angle: Angle) -> Double {
16 | sin(angle.radians)
17 | }
18 |
19 | public func tan(_ angle: Angle) -> Double {
20 | tan(angle.radians)
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/Foundation/Float+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Float+Convenience.swift
3 | //
4 | // Created by Adam Fordyce on 01/11/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | // MARK: ----- PROPERTIES
11 |
12 | public extension Float {
13 |
14 | var abs: Float {
15 | Swift.abs(self)
16 | }
17 | }
18 |
19 | public extension Float {
20 |
21 | func random() -> Float {
22 | self * Float.random(in: 0...1)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/Foundation/Double+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Double+Convenience.swift
3 | //
4 | // Created by Adam Fordyce on 23/10/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | // MARK: ----- PROPERTIES
11 |
12 | public extension Double {
13 |
14 | var abs: Double {
15 | Swift.abs(self)
16 | }
17 | }
18 |
19 | public extension Double {
20 |
21 | func random() -> Double {
22 | self * Double.random(in: 0...1)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Internal/Convenience/Edge+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Edge+Convenience.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 02/02/2020.
6 | // Copyright © 2020 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | // MARK: ----- INSETS
12 |
13 | internal extension Edge.Set {
14 |
15 | func inset(_ edgeSet: Edge.Set, _ size: CGFloat) -> CGFloat {
16 | self.contains(edgeSet) ? size : 0
17 | }
18 |
19 | func hInset(_ size: CGFloat) -> CGFloat {
20 | inset(.horizontal, size)
21 | }
22 |
23 | func vInset(_ size: CGFloat) -> CGFloat {
24 | inset(.vertical, size)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/Foundation/FloatingPoint+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingPoint+Convenience.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 19/11/2019.
6 | // Copyright © 2019 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public extension FloatingPoint {
12 |
13 | var isPositive: Bool {
14 | self >= 0
15 | }
16 |
17 | var isNegative: Bool {
18 | !isPositive
19 | }
20 |
21 | var clampedPositive: Self {
22 | return isPositive ? self : 0
23 | }
24 |
25 | var clampedNegative: Self {
26 | return isPositive ? 0 : self
27 | }
28 | }
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Internal/Modifiers/View+InternalModifiers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 12/07/2021.
6 | //
7 |
8 | internal extension View {
9 |
10 | func geometryReader(_ geoCallback: @escaping (GeometryProxy) -> ()) -> some View {
11 | geometryReader(id: 1, geoCallback)
12 | }
13 |
14 | func geometryReader(id: T, _ geoCallback: @escaping (GeometryProxy) -> ()) -> some View {
15 | overlay(GeometryReader { (geo: GeometryProxy) in
16 | Color.clear.onAppear {
17 | geoCallback(geo)
18 | }
19 | .id(id)
20 | })
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Model/LayoutGuide/BaseLayoutGuideTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LayoutGuideTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 14/02/2020.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class BaseLayoutGuideTests: XCTestCase {
12 |
13 | let x: CGFloat = 2
14 | let y: CGFloat = 4
15 | let width: CGFloat = 20
16 | let height: CGFloat = 12
17 |
18 | var rect: CGRect {
19 | CGRect(x, y, width, height)
20 | }
21 |
22 | var bottomRightRect: CGRect {
23 | CGRect(rect.center, rect.sizeScaled(0.5))
24 | }
25 | }
26 |
27 | class LayoutGuideTests: BaseLayoutGuideTests {
28 |
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/GlobalFunctions/TrigonometryFunctionsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TrigonometryFunctionsTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 06/02/2020.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class TrigonometryFunctionsTests: XCTestCase {
12 |
13 |
14 | }
15 |
16 | // MARK: ----- GENERAL
17 |
18 | extension TrigonometryFunctionsTests {
19 |
20 | func testSin() {
21 | XCTAssertEqual(sin(30.degrees), sin(Angle.degrees(30).radians))
22 | }
23 |
24 | func testCos() {
25 | XCTAssertEqual(cos(30.degrees), cos(Angle.degrees(30).radians))
26 | }
27 |
28 | func testTan() {
29 | XCTAssertEqual(tan(30.degrees), tan(Angle.degrees(30).radians))
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/SwiftUI/Edge+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Edge+ConvenienceTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 06/02/2020.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class EdgeSetConvenienceExtensionsTests: XCTestCase {
12 |
13 | let size: CGFloat = 2
14 | }
15 |
16 | // MARK: ----- CONVENIENCE FUNCTIONS
17 |
18 | extension EdgeSetConvenienceExtensionsTests {
19 |
20 | func testInset() {
21 | XCTAssertEqual(Edge.Set.horizontal.inset(.horizontal, size), size)
22 | XCTAssertEqual(Edge.Set.horizontal.inset(.vertical, size), 0)
23 |
24 | XCTAssertEqual(Edge.Set.horizontal.hInset(size), size)
25 | XCTAssertEqual(Edge.Set.horizontal.vInset(size), 0)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Protocols/Types/RepresentableAsInt.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepresentableAsInt.swift
3 | //
4 | // Created by Adam Fordyce on 19/11/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | public protocol RepresentableAsInt {
11 | var asInt: Int {get}
12 | }
13 |
14 | extension Int: RepresentableAsInt {
15 | public var asInt: Int {
16 | self
17 | }
18 | }
19 |
20 | extension Double: RepresentableAsInt {
21 | public var asInt: Int {
22 | Int(self)
23 | }
24 | }
25 |
26 | extension Float: RepresentableAsInt {
27 | public var asInt: Int {
28 | Int(self)
29 | }
30 | }
31 |
32 | extension CGFloat: RepresentableAsInt {
33 | public var asInt: Int {
34 | Int(self)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "PureSwiftUIDesign",
8 | platforms: [
9 | .iOS(.v13),
10 | .watchOS(.v6),
11 | .tvOS(.v13),
12 | .macOS(.v10_15)
13 | ],
14 | products: [
15 | .library(
16 | name: "PureSwiftUIDesign",
17 | targets: ["PureSwiftUIDesign"]),
18 | ],
19 | dependencies: [
20 | ],
21 | targets: [
22 | .target(
23 | name: "PureSwiftUIDesign",
24 | dependencies: []),
25 | .testTarget(
26 | name: "PureSwiftUIDesignTests",
27 | dependencies: ["PureSwiftUIDesign"]),
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/CoreGraphics/CGFloat+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGFloat+Convenience.swift
3 | //
4 | // Created by Adam Fordyce on 24/10/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | // MARK: ----- PROPERTIES
11 |
12 | public extension CGFloat {
13 |
14 | var abs: CGFloat {
15 | Swift.abs(self)
16 | }
17 | }
18 |
19 | // MARK: ----- UTILITY FUNCTIONS
20 |
21 | public extension CGFloat {
22 |
23 | func random() -> CGFloat {
24 | self * CGFloat.random(in: 0...1)
25 | }
26 | }
27 |
28 | // MARK: ----- TO WITH FACTOR
29 |
30 | public extension CGFloat {
31 |
32 | func to(_ destination: CGFloat, _ factor: CGFloat) -> CGFloat {
33 | CGFloat(self + (destination - self) * factor)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Protocols/Types/RepresentableAsFloat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepresentableAsFloat.swift
3 | //
4 | // Created by Adam Fordyce on 19/11/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | public protocol RepresentableAsFloat {
11 | var asFloat: Float {get}
12 | }
13 |
14 | extension Int: RepresentableAsFloat {
15 | public var asFloat: Float {
16 | Float(self)
17 | }
18 | }
19 |
20 | extension Double: RepresentableAsFloat {
21 | public var asFloat: Float {
22 | Float(self)
23 | }
24 | }
25 |
26 | extension Float: RepresentableAsFloat {
27 | public var asFloat: Float {
28 | self
29 | }
30 | }
31 |
32 | extension CGFloat: RepresentableAsFloat {
33 | public var asFloat: Float {
34 | Float(self)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Protocols/Types/RepresentableAsDouble.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepresentableAsDouble.swift
3 | //
4 | // Created by Adam Fordyce on 19/11/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | public protocol RepresentableAsDouble {
11 | var asDouble: Double {get}
12 | }
13 |
14 | extension Int: RepresentableAsDouble {
15 | public var asDouble: Double {
16 | Double(self)
17 | }
18 | }
19 |
20 | extension Float: RepresentableAsDouble {
21 | public var asDouble: Double {
22 | Double(self)
23 | }
24 | }
25 |
26 | extension Double: RepresentableAsDouble {
27 | public var asDouble: Double {
28 | self
29 | }
30 | }
31 |
32 | extension CGFloat: RepresentableAsDouble {
33 | public var asDouble: Double {
34 | Double(self)
35 | }
36 | }
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/Foundation/Double+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Double+Tests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 27/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class DoubleConvenienceExtensionsTests: XCTestCase {
12 |
13 | func testRandom() {
14 |
15 | let value: Double = 10
16 |
17 | for _ in 0...10 {
18 | let result = value.random()
19 |
20 | XCTAssertTrue(result <= 10 && result >= 0)
21 | }
22 | }
23 | }
24 |
25 | // MARK: ----- PROPERTIES
26 |
27 | extension DoubleConvenienceExtensionsTests {
28 |
29 | func testAbs() {
30 | XCTAssertEqual((-10).asDouble.abs, 10)
31 | XCTAssertEqual(0.asDouble.abs, 0)
32 | XCTAssertEqual(10.asDouble.abs, 10)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Protocols/Types/RepresentableAsCGFloat.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepresentableAsCGFloat.swift
3 | //
4 | // Created by Adam Fordyce on 19/11/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | public protocol RepresentableAsCGFloat {
11 | var asCGFloat: CGFloat {get}
12 | }
13 |
14 | extension Int: RepresentableAsCGFloat {
15 | public var asCGFloat: CGFloat {
16 | CGFloat(self)
17 | }
18 | }
19 |
20 | extension Float: RepresentableAsCGFloat {
21 | public var asCGFloat: CGFloat {
22 | CGFloat(self)
23 | }
24 | }
25 |
26 | extension Double: RepresentableAsCGFloat {
27 | public var asCGFloat: CGFloat {
28 | CGFloat(self)
29 | }
30 | }
31 |
32 | extension CGFloat: RepresentableAsCGFloat {
33 | public var asCGFloat: CGFloat {
34 | self
35 | }
36 | }
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Assets/Docs/LICENCE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright © 2019 Adam Fordyce
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/Foundation/FloatingPoint+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingPoint+Tests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 27/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class FloatingPointConvenienceExtensionsTests: XCTestCase {
12 |
13 | func testIsPositive() {
14 |
15 | XCTAssertTrue(1.0.isPositive)
16 | XCTAssertFalse((-1.0).isPositive)
17 | }
18 |
19 | func testIsNegative() {
20 |
21 | XCTAssertFalse(1.0.isNegative)
22 | XCTAssertTrue((-1.0).isNegative)
23 | }
24 |
25 | func testClampPositive() {
26 | XCTAssertEqual((-1.0).clampedPositive, 0.0)
27 | XCTAssertEqual(1.0.clampedPositive, 1.0)
28 | }
29 |
30 | func testClampNegative() {
31 | XCTAssertEqual(1.0.clampedNegative, 0.0)
32 | XCTAssertEqual((-1.0).clampedNegative, -1.0)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/Foundation/Float+Angle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Float+Angle.swift
3 | //
4 | // Created by Adam Fordyce on 01/11/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public extension Float {
11 |
12 | var degrees: Angle {
13 | .degrees(asDouble)
14 | }
15 |
16 | var radians: Angle {
17 | .radians(asDouble)
18 | }
19 |
20 | var cycles: Angle {
21 | .cycles(asDouble)
22 | }
23 |
24 | var degreesAsRadians: Angle {
25 | asDouble.degreesAsRadians
26 | }
27 |
28 | var radiansAsDegrees: Angle {
29 | asDouble.radiansAsDegrees
30 | }
31 |
32 | var acos: Angle {
33 | Darwin.acos(self).radians
34 | }
35 |
36 | var asin: Angle {
37 | Darwin.asin(self).radians
38 | }
39 |
40 | var atan: Angle {
41 | Darwin.atan(self).radians
42 | }
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/CoreGraphics/CGFloat+Angle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGFloat+Angle.swift
3 | //
4 | // Created by Adam Fordyce on 24/10/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public extension CGFloat {
11 |
12 | var degrees: Angle {
13 | .degrees(asDouble)
14 | }
15 |
16 | var radians: Angle {
17 | .radians(asDouble)
18 | }
19 |
20 | var cycles: Angle {
21 | .cycles(asDouble)
22 | }
23 |
24 | var degreesAsRadians: Angle {
25 | asDouble.degreesAsRadians
26 | }
27 |
28 | var radiansAsDegrees: Angle {
29 | asDouble.radiansAsDegrees
30 | }
31 |
32 | var acos: Angle {
33 | Darwin.acos(asDouble).radians
34 | }
35 |
36 | var asin: Angle {
37 | Darwin.asin(asDouble).radians
38 | }
39 |
40 | var atan: Angle {
41 | Darwin.atan(asDouble).radians
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/Foundation/Comparable+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Comparable+Convenience.swift
3 | //
4 | // Created by Adam Fordyce on 05/11/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | public extension Comparable {
9 |
10 | func clamped(min: Self) -> Self {
11 | clamped(min: min, max: self)
12 | }
13 |
14 | func clamped(max: Self) -> Self {
15 | clamped(min: self, max: max)
16 | }
17 |
18 | func clamped(max: Self, abs clampAbsoluteValue: Bool) -> Self where Self: SignedNumeric {
19 | if clampAbsoluteValue {
20 | return clamped(min: -abs(max), max: abs(max))
21 | } else {
22 | return clamped(max: max)
23 | }
24 | }
25 |
26 | func clamped(min: Self, max: Self) -> Self {
27 | if (self > max) {
28 | return max
29 | } else if (self < min) {
30 | return min
31 | }
32 | return self
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/Foundation/Int+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Int+Convenience.swift
3 | //
4 | // Created by Adam Fordyce on 23/10/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public extension Int {
11 |
12 | var asString: String {
13 | String(self)
14 | }
15 |
16 | var isPositive: Bool {
17 | return self >= 0
18 | }
19 |
20 | var clampedPositive: Int {
21 | return isPositive ? self : 0
22 | }
23 |
24 | var clampedNegative: Int {
25 | return isPositive ? 0 : self
26 | }
27 |
28 | var isNegative: Bool {
29 | !isPositive
30 | }
31 |
32 | var isEven: Bool {
33 | self.isMultiple(of: 2)
34 | }
35 |
36 | var isOdd: Bool {
37 | !isEven
38 | }
39 |
40 | var abs: Int {
41 | Swift.abs(self)
42 | }
43 |
44 | func random() -> Int {
45 | Int.random(in: 0...self)
46 | }
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/Foundation/Double+Angle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Double+Angle.swift
3 | //
4 | // Created by Adam Fordyce on 23/10/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import SwiftUI
9 |
10 | private let degreesAsRadiansFactor = Double.pi / 180
11 | private let radiansAsDegreesFactor = 180 / Double.pi
12 |
13 | public extension Double {
14 |
15 | var degrees: Angle {
16 | .degrees(self)
17 | }
18 |
19 | var radians: Angle {
20 | .radians(self)
21 | }
22 |
23 | var cycles: Angle {
24 | .cycles(self)
25 | }
26 |
27 | var degreesAsRadians: Angle {
28 | (self * degreesAsRadiansFactor).radians
29 | }
30 |
31 | var radiansAsDegrees: Angle {
32 | (self * radiansAsDegreesFactor).degrees
33 | }
34 |
35 | var acos: Angle {
36 | Darwin.acos(self).radians
37 | }
38 |
39 | var asin: Angle {
40 | Darwin.asin(self).radians
41 | }
42 |
43 | var atan: Angle {
44 | Darwin.atan(self).radians
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/GlobalFunctions/GeometryFunctions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeometryFunctions.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 28/10/2019.
6 | // Copyright © 2019 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public func calcXOffset(radius: CGFloat, angle: Angle) -> CGFloat {
12 | (angle.cos * abs(radius.asDouble)).asCGFloat
13 | }
14 |
15 | public func calcYOffset(radius: CGFloat, angle: Angle) -> CGFloat {
16 | (angle.sin * abs(radius.asDouble)).asCGFloat
17 | }
18 |
19 | //public func calcOffset(radius: CGFloat, angle: Angle) -> CGPoint {
20 | // let absRadius = abs(radius.asDouble)
21 | // let xOffset: Double = absRadius * angle.sin
22 | // let yOffset: Double = absRadius * angle.cos * -1
23 | // return CGPoint(xOffset.asCGFloat, yOffset.asCGFloat)
24 | //}
25 |
26 | public func calcOffset(radius: CGFloat, angle: Angle) -> CGPoint {
27 | let absRadius = abs(radius.asDouble)
28 | let xOffset: Double = absRadius * angle.cos
29 | let yOffset: Double = absRadius * angle.sin
30 | return CGPoint(xOffset.asCGFloat, yOffset.asCGFloat)
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/Foundation/Float+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Float+Tests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 27/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class FloatConvenienceExtensionsTests: XCTestCase {
12 |
13 | func testRepresentableAs() {
14 | XCTAssertEqual(Int(1).asFloat, Float(1))
15 | XCTAssertEqual(CGFloat(1).asFloat, Float(1))
16 | XCTAssertEqual(Double(1).asFloat, Float(1))
17 | XCTAssertEqual(Float(1).asFloat, Float(1))
18 | }
19 |
20 | func testRandom() {
21 |
22 | let value: Float = 10
23 |
24 | for _ in 0...10 {
25 | let result = value.random()
26 |
27 | XCTAssertTrue(result <= 10 && result >= 0)
28 | }
29 | }
30 | }
31 |
32 | // MARK: ----- PROPERTIES
33 |
34 | extension FloatConvenienceExtensionsTests {
35 |
36 | func testAbs() {
37 | XCTAssertEqual((-10).asFloat.abs, 10)
38 | XCTAssertEqual(0.asFloat.abs, 0)
39 | XCTAssertEqual(10.asFloat.abs, 10)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/CoreGraphics/CGAffineTransform+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGAffineTransform+Convenience.swift
3 | //
4 | //
5 | // Created by NTB on 27/01/2020.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public extension CGAffineTransform {
11 |
12 | static func rotation(_ angle: Angle) -> CGAffineTransform {
13 | .init(rotationAngle: angle.radians.asCGFloat)
14 | }
15 |
16 | static func translation(_ x: CGFloat, _ y: CGFloat) -> CGAffineTransform {
17 | .init(translationX: x, y: y)
18 | }
19 |
20 | static func xTranslation(_ x: CGFloat) -> CGAffineTransform {
21 | .init(translationX: x, y: 0)
22 | }
23 |
24 | static func yTranslation(_ y: CGFloat) -> CGAffineTransform {
25 | .init(translationX: 0, y: y)
26 | }
27 |
28 | static func scale(_ x: CGFloat, _ y: CGFloat) -> CGAffineTransform {
29 | .init(scaleX: x, y: y)
30 | }
31 |
32 | static func xScale(_ x: CGFloat) -> CGAffineTransform {
33 | .init(scaleX: x, y: 1)
34 | }
35 |
36 | static func yScale(_ y: CGFloat) -> CGAffineTransform {
37 | .init(scaleX: 1, y: y)
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/GlobalFunctions/GeometryFunctionsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeometryFunctionsTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 16/02/2020.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class GeometryFunctionsTests: XCTestCase {
12 |
13 |
14 | }
15 |
16 | // MARK: ----- OFFSETS FOR ANGLES
17 |
18 | extension GeometryFunctionsTests {
19 |
20 | func testCalcOffsetTest() {
21 | assertEqual(calcOffset(radius: 10, angle: 0.degrees), .point(10, 0))
22 | assertEqual(calcOffset(radius: 10, angle: 90.degrees), .point(0, 10))
23 | assertEqual(calcOffset(radius: 10, angle: 180.degrees), .point(-10, 0))
24 | assertEqual(calcOffset(radius: 10, angle: 270.degrees), .point(0, -10))
25 | }
26 |
27 | func testCalcOffsetTestFromTop() {
28 | assertEqual(calcOffset(radius: 10, angle: 0.degrees.fromTop), .point(0, -10))
29 | assertEqual(calcOffset(radius: 10, angle: 90.degrees.fromTop), .point(10, -0))
30 | assertEqual(calcOffset(radius: 10, angle: 180.degrees.fromTop), .point(0, 10))
31 | assertEqual(calcOffset(radius: 10, angle: 270.degrees.fromTop), .point(-10, 0))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/Foundation/Int+Angle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Int+Angleswift
3 | //
4 | // Created by Adam Fordyce on 23/10/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public extension Int {
11 |
12 | var degree: Angle {
13 | .degrees(asDouble)
14 | }
15 |
16 | var degrees: Angle {
17 | .degrees(asDouble)
18 | }
19 |
20 | var radian: Angle {
21 | .radians(asDouble)
22 | }
23 |
24 | var radians: Angle {
25 | .radians(asDouble)
26 | }
27 |
28 | var cycle: Angle {
29 | .cycles(asDouble)
30 | }
31 |
32 | var cycles: Angle {
33 | .cycles(asDouble)
34 | }
35 |
36 | var degreesAsRadians: Angle {
37 | asDouble.degreesAsRadians
38 | }
39 |
40 | var radiansAsDegrees: Angle {
41 | asDouble.radiansAsDegrees
42 | }
43 |
44 | var acos: Angle {
45 | Darwin.acos(asDouble).radians
46 | }
47 |
48 | var asin: Angle {
49 | Darwin.asin(asDouble).radians
50 | }
51 |
52 | var atan: Angle {
53 | Darwin.atan(asDouble).radians
54 | }
55 | }
56 |
57 |
58 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/CoreGraphics/CGFloat+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGFloat+Tests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 25/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class CGFloatConvenienceExtensionsTests: XCTestCase {
12 |
13 | func testRandom() {
14 |
15 | let value: CGFloat = 10
16 |
17 | for _ in 0...10 {
18 | let result = value.random()
19 |
20 | XCTAssertTrue(result <= 10 && result >= 0)
21 | }
22 | }
23 | }
24 |
25 | // MARK: ----- PROPERTIES
26 |
27 | extension CGFloatConvenienceExtensionsTests {
28 |
29 | func testAbs() {
30 | XCTAssertEqual((-10).asCGFloat.abs, 10)
31 | XCTAssertEqual(0.asCGFloat.abs, 0)
32 | XCTAssertEqual(10.asCGFloat.abs, 10)
33 | }
34 | }
35 |
36 | // MARK: ----- TO
37 |
38 | extension CGFloatConvenienceExtensionsTests {
39 |
40 | func testToWithFactor() {
41 | let valueFrom: CGFloat = 5
42 | let valueTo: CGFloat = 10
43 | XCTAssertEqual(valueFrom.to(valueTo, 0), 5)
44 | XCTAssertEqual(valueFrom.to(valueTo, 0.5), 7.5)
45 | XCTAssertEqual(valueFrom.to(valueTo, 1), 10)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/SwiftUI/UnitPoint+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnitPoint+Convenience.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 13/01/2020.
6 | // Copyright © 2020 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension UnitPoint {
12 |
13 | init(_ x: CGFloat, _ y: CGFloat) {
14 | self.init(x: x, y: y)
15 | }
16 |
17 | var asCGPoint: CGPoint {
18 | return CGPoint(x, y)
19 | }
20 |
21 | func inverted() -> UnitPoint {
22 | UnitPoint(1 - x, 1 - y)
23 | }
24 | }
25 |
26 | // MARK: ----- ANGLE CONVERSION
27 |
28 | private let maxUnitRadius = sqrt(0.5 * 0.5 + 0.5 * 0.5)
29 | private let centerPoint = CGPoint(0.5, 0.5)
30 |
31 | private let angleForNamedUnitPoint: [UnitPoint: Angle] = [
32 |
33 | .topLeading: .topLeading,
34 | .top: .top,
35 | .topTrailing: .topTrailing,
36 | .trailing: .trailing,
37 | .bottomTrailing: .bottomTrailing,
38 | .bottom: .bottom,
39 | .bottomLeading: .bottomLeading,
40 | .leading: .leading,
41 | ]
42 |
43 | public extension UnitPoint {
44 |
45 | var asAngle: Angle {
46 | if let angle = angleForNamedUnitPoint[self] {
47 | return angle
48 | } else {
49 | return centerPoint.angleTo(self.asCGPoint)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Model/LayoutGuide/LayoutGuideEnvironment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LayoutGuideEnvironment.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 04/02/2020.
6 | // Copyright © 2020 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct ShowLayoutGuidesKey: EnvironmentKey {
12 | public static let defaultValue: Bool = false
13 | }
14 |
15 | public extension EnvironmentValues {
16 | var showLayoutGuides: Bool {
17 | get {
18 | self[ShowLayoutGuidesKey.self]
19 | }
20 | set {
21 | self[ShowLayoutGuidesKey.self] = newValue
22 | }
23 | }
24 | }
25 |
26 | public struct ShowControlPoints: EnvironmentKey {
27 | public static let defaultValue: Bool = false
28 | }
29 |
30 | public extension EnvironmentValues {
31 | var showControlPoints: Bool {
32 | get {
33 | self[ShowControlPoints.self]
34 | }
35 | set {
36 | self[ShowControlPoints.self] = newValue
37 | }
38 | }
39 | }
40 |
41 | // MARK: ----- VIEW EXTENSIONS
42 |
43 | public extension View {
44 |
45 | func showLayoutGuides(_ value: Bool) -> some View {
46 | environment(\.showLayoutGuides, value)
47 | }
48 |
49 | func showControlPoints(_ value: Bool) -> some View {
50 | environment(\.showControlPoints, value)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/CoreGraphics/CGAffineTransform+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGAffineTransform+ConvenienceTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 06/02/2020.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class CGAffineTransformConvenienceExtensionsTests: XCTestCase {
12 |
13 | let x: CGFloat = 2
14 | let y: CGFloat = 4
15 | let angle = 15.degrees
16 | }
17 |
18 | // MARK: ----- STATIC CONSTRUCTORS
19 |
20 | extension CGAffineTransformConvenienceExtensionsTests {
21 |
22 | func testRotation() {
23 | XCTAssertEqual(CGAffineTransform.rotation(angle), CGAffineTransform(rotationAngle: angle.radians.asCGFloat))
24 | }
25 |
26 | func testTranslation() {
27 | XCTAssertEqual(CGAffineTransform.translation(x, y), CGAffineTransform(translationX: x, y: y))
28 | XCTAssertEqual(CGAffineTransform.xTranslation(x), CGAffineTransform(translationX: x, y: 0))
29 | XCTAssertEqual(CGAffineTransform.yTranslation(y), CGAffineTransform(translationX: 0, y: y))
30 | }
31 |
32 | func testScale() {
33 | XCTAssertEqual(CGAffineTransform.scale(2, 0.5), CGAffineTransform(scaleX: 2, y: 0.5))
34 | XCTAssertEqual(CGAffineTransform.xScale(2), CGAffineTransform(scaleX: 2, y: 1))
35 | XCTAssertEqual(CGAffineTransform.yScale(0.5), CGAffineTransform(scaleX: 1, y: 0.5))
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/Foundation/Comparable+Tests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Comparable+Tests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 27/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class ComparableConvenienceExtensionsTests: XCTestCase {
12 |
13 | func testFrom() {
14 | XCTAssertEqual(2, 2.clamped(min: 0))
15 | XCTAssertEqual(4, 3.clamped(min: 4))
16 | XCTAssertEqual(2, 2.clamped(min: 0))
17 | XCTAssertEqual(2, 2.clamped(min: -3))
18 | }
19 |
20 | func testTo() {
21 | XCTAssertEqual(1, 2.clamped(max: 1))
22 | XCTAssertEqual(3, 3.clamped(max: 10))
23 | XCTAssertEqual(2, 2.clamped(max: 3))
24 | XCTAssertEqual(-3, 2.clamped(max: -3))
25 | }
26 |
27 | func testClamped() {
28 | XCTAssertEqual(1, 2.clamped(min: 0, max: 1))
29 | XCTAssertEqual(4, 3.clamped(min: 4, max: 10))
30 | XCTAssertEqual(2, 2.clamped(min: 0, max: 3))
31 | XCTAssertEqual(2, 2.clamped(min: -3, max: 3))
32 | }
33 |
34 | func testClampedToAbs() {
35 | XCTAssertEqual(0, 2.clamped(max: 0, abs: true))
36 | XCTAssertEqual(-3, (-4).clamped(max: 3, abs: true))
37 | XCTAssertEqual(-3, (-3).clamped(max: 4, abs: true))
38 | XCTAssertEqual(0, (-12).clamped(max: 0, abs: true))
39 | XCTAssertEqual(2, 2.clamped(max: -3, abs: true))
40 | }
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/PureSwiftUIDesign.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = "PureSwiftUIDesign"
3 | spec.version = "1.0.0"
4 | spec.summary = "PureSwiftUIDesign is a Swift package designed to enhance the experience of designing views and shapes in SwiftUI."
5 | spec.description = <<-DESC
6 | 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.
7 |
8 | This package allows you to create incredibly complex and even animated designs quickly, while keeping the code simple.
9 | DESC
10 | spec.homepage = "https://github.com/CodeSlicing/pure-swift-ui-design"
11 | spec.license = { :type => "MIT", :file => "Assets/Docs/LICENCE.md" }
12 | spec.author = "Adam Fordyce"
13 | spec.social_media_url = "https://twitter.com/CodeSlice"
14 |
15 | spec.ios.deployment_target = "13.0"
16 | spec.macos.deployment_target = "10.15"
17 | spec.tvos.deployment_target = "13.0"
18 | spec.watchos.deployment_target = "6.0"
19 |
20 | spec.source = { :git => "https://github.com/CodeSlicing/pure-swift-ui-design.git", :tag => "#{spec.version}" }
21 |
22 | spec.source_files = "Sources/**/*.{swift}"
23 | spec.swift_version = "5.1"
24 | spec.framework = "SwiftUI"
25 | end
26 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/UIKit/UIScreentConvenience+Tests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIScreen+ConvenienceTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 18/12/2019.
6 | //
7 |
8 | #if canImport(UIKit) && !os(watchOS)
9 | import XCTest
10 | @testable import PureSwiftUIDesign
11 |
12 | class UIScreenConvenienceExtensionsTests: XCTestCase {
13 |
14 | func testOrigin() {
15 | XCTAssertEqual(UIScreen.main.origin, UIScreen.main.bounds.origin)
16 | }
17 |
18 | func testCenter() {
19 | XCTAssertEqual(UIScreen.main.center, UIScreen.main.bounds.center)
20 | }
21 |
22 | func testMinX() {
23 | XCTAssertEqual(UIScreen.main.minX, UIScreen.main.bounds.minX)
24 | }
25 |
26 | func testMinY() {
27 | XCTAssertEqual(UIScreen.main.minY, UIScreen.main.bounds.minY)
28 | }
29 |
30 | func testMaxX() {
31 | XCTAssertEqual(UIScreen.main.maxX, UIScreen.main.bounds.maxX)
32 | }
33 |
34 | func testMaxY() {
35 | XCTAssertEqual(UIScreen.main.maxY, UIScreen.main.bounds.maxY)
36 | }
37 |
38 | func testMidX() {
39 | XCTAssertEqual(UIScreen.main.midX, UIScreen.main.bounds.midX)
40 | }
41 |
42 | func testMidY() {
43 | XCTAssertEqual(UIScreen.main.midY, UIScreen.main.bounds.midY)
44 | }
45 |
46 | func testWidth() {
47 | XCTAssertEqual(UIScreen.main.width, UIScreen.main.bounds.width)
48 | }
49 |
50 | func testHeight() {
51 | XCTAssertEqual(UIScreen.main.height, UIScreen.main.bounds.height)
52 | }
53 |
54 | func testSize() {
55 | XCTAssertEqual(UIScreen.main.size, UIScreen.main.bounds.size)
56 | }
57 | }
58 | #endif
59 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Util/TestFunctions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestFunctions.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 06/02/2020.
6 | //
7 |
8 | import XCTest
9 | import PureSwiftUIDesign
10 |
11 | func assertEqual(_ result: UnitPoint, _ expectedResult: UnitPoint, file: StaticString = #file, line: UInt = #line) {
12 | assertEqual(result.asCGPoint, expectedResult.asCGPoint, file: file, line: line)
13 | }
14 |
15 | func assertEqual(_ result: Double, _ expectedResult: Double, file: StaticString = #file, line: UInt = #line) {
16 | XCTAssertEqual(result, expectedResult, accuracy: 0.0001, file: file, line: line)
17 | }
18 |
19 | func assertEqual(_ result: CGPoint, _ expected: (x: CGFloat, y: CGFloat), file: StaticString = #file, line: UInt = #line) {
20 | assertEqual(result, CGPoint(expected.x, expected.y), file: file, line: line)
21 | }
22 |
23 | func assertEqual(_ result: CGPoint, _ expected: CGPoint, file: StaticString = #file, line: UInt = #line) {
24 | XCTAssertEqual(result.rounded(toPlaces: 2), expected.rounded(toPlaces: 2), file: file, line: line)
25 | }
26 |
27 | func assertEqual(_ result: CGSize, _ expected: CGSize, file: StaticString = #file, line: UInt = #line) {
28 | XCTAssertEqual(result.rounded(toPlaces: 2), expected.rounded(toPlaces: 2), file: file, line: line)
29 | }
30 |
31 | private extension CGPoint {
32 | /// Rounds the double to decimal places value
33 | func rounded(toPlaces places:Int) -> CGPoint {
34 | CGPoint(round(self.x, to: 2), round(self.y, to: 2))
35 | }
36 | }
37 |
38 | private extension CGSize {
39 | /// Rounds the double to decimal places value
40 | func rounded(toPlaces places:Int) -> CGSize {
41 | CGSize(round(self.width, to: 2), round(self.height, to: 2))
42 | }
43 | }
44 |
45 | private func round(_ value: CGFloat, to places: Int) -> CGFloat {
46 |
47 | let divisor = pow(10.0, CGFloat(places))
48 | return (value * divisor).rounded() / divisor
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/Foundation/Float+AngleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Float+AngleTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 26/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class FloatAngleExtensionsTests: XCTestCase {
12 |
13 | let degreesValue: Double = 45
14 | let radiansValue: Double = 2
15 | let inverseValue: Double = 1
16 |
17 | func testFloatAsDegrees() {
18 | let expectedResult = Angle.degrees(degreesValue)
19 | let result = Float(degreesValue).degrees
20 | XCTAssertEqual(result, expectedResult)
21 | }
22 |
23 | func testFloatAsRadians() {
24 | let expectedResult = Angle.radians(radiansValue)
25 | let result = Float(radiansValue).radians
26 | XCTAssertEqual(result, expectedResult)
27 | }
28 |
29 | func testFloatAsCycles() {
30 | XCTAssertEqual(Float(0).cycles, Angle.degrees(0))
31 | XCTAssertEqual(Float(0.5).cycles, Angle.degrees(180))
32 | XCTAssertEqual(Float(1.0).cycles, Angle.degrees(360))
33 | XCTAssertEqual(Float(-0.75).cycles, Angle.degrees(-270))
34 | }
35 |
36 | func testFloatDegreesAsRadians() {
37 | let expectedResult = Angle.radians(Angle.degrees(degreesValue).radians)
38 | let result = Float(degreesValue).degreesAsRadians
39 | XCTAssertEqual(result, expectedResult)
40 | }
41 |
42 | func testFloatRadiansAsDegrees() {
43 | let expectedResult = Angle.degrees(Angle.radians(radiansValue).degrees)
44 | let result = Float(radiansValue).radiansAsDegrees
45 | XCTAssertEqual(result, expectedResult)
46 | }
47 |
48 | func testFloatACos() {
49 | let expectedResult = acos(inverseValue)
50 | let result = Float(inverseValue).acos.radians
51 | XCTAssertEqual(result, expectedResult)
52 | }
53 |
54 | func testFloatASin() {
55 | let expectedResult = asin(inverseValue)
56 | let result = Float(inverseValue).asin.radians
57 | assertEqual(result, expectedResult)
58 | }
59 |
60 | func testFloatATan() {
61 | let expectedResult = atan(inverseValue)
62 | let result = Float(inverseValue).atan.radians
63 | assertEqual(result, expectedResult)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/Foundation/Double+AngleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Double+AngleTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 26/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class DoubleAngleExtensionsTests: XCTestCase {
12 |
13 | let degreesValue: Double = 45
14 | let radiansValue: Double = 2
15 | let inverseValue: Double = 1
16 |
17 | func testDoubleAsDegrees() {
18 | let expectedResult = Angle.degrees(degreesValue)
19 | let result = Double(degreesValue).degrees
20 | XCTAssertEqual(result, expectedResult)
21 | }
22 |
23 | func testDoubleAsRadians() {
24 | let expectedResult = Angle.radians(radiansValue)
25 | let result = Double(radiansValue).radians
26 | XCTAssertEqual(result, expectedResult)
27 | }
28 |
29 | func testDoubleAsCycles() {
30 | XCTAssertEqual(Double(0).cycles, Angle.degrees(0))
31 | XCTAssertEqual(Double(0.5).cycles, Angle.degrees(180))
32 | XCTAssertEqual(Double(1.0).cycles, Angle.degrees(360))
33 | XCTAssertEqual(Double(-0.75).cycles, Angle.degrees(-270))
34 | }
35 |
36 | func testDoubleDegreesAsRadians() {
37 | let expectedResult = Angle.radians(Angle.degrees(degreesValue).radians)
38 | let result = Double(degreesValue).degreesAsRadians
39 | XCTAssertEqual(result, expectedResult)
40 | }
41 |
42 | func testDoubleRadiansAsDegrees() {
43 | let expectedResult = Angle.degrees(Angle.radians(radiansValue).degrees)
44 | let result = Double(radiansValue).radiansAsDegrees
45 | XCTAssertEqual(result, expectedResult)
46 | }
47 |
48 | func testDoubleACos() {
49 | let expectedResult = acos(inverseValue)
50 | let result = Double(inverseValue).acos.radians
51 | XCTAssertEqual(result, expectedResult)
52 | }
53 |
54 | func testDoubleASin() {
55 | let expectedResult = asin(inverseValue)
56 | let result = Double(inverseValue).asin.radians
57 | XCTAssertEqual(result, expectedResult)
58 | }
59 |
60 | func testDoubleATan() {
61 | let expectedResult = atan(inverseValue)
62 | let result = Double(inverseValue).atan.radians
63 | XCTAssertEqual(result, expectedResult)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/CoreGraphics/CGFloat+AngleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGFloat+AngleTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 26/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class CGFloatAngleExtensionsTests: XCTestCase {
12 |
13 | let degreesValue: Double = 45
14 | let radiansValue: Double = 2
15 | let inverseValue: Double = 1
16 |
17 | func testCGFloatAsDegrees() {
18 | let expectedResult = Angle.degrees(degreesValue)
19 | let result = CGFloat(degreesValue).degrees
20 | XCTAssertEqual(result, expectedResult)
21 | }
22 |
23 | func testCGFloatAsRadians() {
24 | let expectedResult = Angle.radians(radiansValue)
25 | let result = CGFloat(radiansValue).radians
26 | XCTAssertEqual(result, expectedResult)
27 | }
28 |
29 | func testCGFloatAsCycles() {
30 | XCTAssertEqual(CGFloat(0).cycles, Angle.cycles(0))
31 | XCTAssertEqual(CGFloat(0.5).cycles, Angle.cycles(0.5))
32 | XCTAssertEqual(CGFloat(1.0).cycles, Angle.cycles(1.0))
33 | XCTAssertEqual(CGFloat(-0.75).cycles, Angle.cycles(-0.75))
34 | }
35 |
36 | func testCGFloatDegreesAsRadians() {
37 | let expectedResult = Angle.radians(Angle.degrees(degreesValue).radians)
38 | let result = CGFloat(degreesValue).degreesAsRadians
39 | XCTAssertEqual(result, expectedResult)
40 | }
41 |
42 | func testCGFloatRadiansAsDegrees() {
43 | let expectedResult = Angle.degrees(Angle.radians(radiansValue).degrees)
44 | let result = CGFloat(radiansValue).radiansAsDegrees
45 | XCTAssertEqual(result, expectedResult)
46 | }
47 |
48 | func testCGFloatACos() {
49 | let expectedResult = acos(inverseValue)
50 | let result = CGFloat(inverseValue).acos.radians
51 | XCTAssertEqual(result, expectedResult)
52 | }
53 |
54 | func testCGFloatASin() {
55 | let expectedResult = asin(inverseValue)
56 | let result = CGFloat(inverseValue).asin.radians
57 | XCTAssertEqual(result, expectedResult)
58 | }
59 |
60 | func testCGFloatATan() {
61 | let expectedResult = atan(inverseValue)
62 | let result = CGFloat(inverseValue).atan.radians
63 | XCTAssertEqual(result, expectedResult)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/Foundation/Int+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Int+Tests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 27/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class IntConvenienceExtensionsTests: XCTestCase {
12 |
13 | }
14 |
15 | // MARK: ----- PROPERTIES
16 |
17 | extension IntConvenienceExtensionsTests {
18 |
19 | func testIsPositive() {
20 |
21 | XCTAssertTrue(1.isPositive)
22 | XCTAssertFalse((-1).isPositive)
23 | }
24 |
25 | func testIsNegative() {
26 |
27 | XCTAssertFalse(1.isNegative)
28 | XCTAssertTrue((-1).isNegative)
29 | }
30 |
31 | func testClampPositive() {
32 | XCTAssertEqual(0, (-1).clampedPositive)
33 | XCTAssertEqual(1, 1.clampedPositive)
34 | }
35 |
36 | func testClampNegative() {
37 | XCTAssertEqual(0, 1.clampedNegative)
38 | XCTAssertEqual(-1, (-1).clampedNegative)
39 | }
40 |
41 | func testIsEven() {
42 | XCTAssertTrue(2.isEven)
43 | XCTAssertFalse(1.isEven)
44 | }
45 |
46 | func testIsOdd() {
47 | XCTAssertFalse(2.isOdd)
48 | XCTAssertTrue(1.isOdd)
49 | }
50 |
51 | func testRepresentableAs() {
52 | XCTAssertEqual(Int(1).asInt, Int(1))
53 | XCTAssertEqual(CGFloat(1).asInt, Int(1))
54 | XCTAssertEqual(Double(1).asInt, Int(1))
55 | XCTAssertEqual(Float(1).asInt, Int(1))
56 | XCTAssertEqual(Double(1.4).asInt, Int(1))
57 | XCTAssertEqual(Float(1.7).asInt, Int(1))
58 | XCTAssertEqual(Float(-1.7).asInt, Int(-1))
59 | }
60 |
61 | // MARK: ----- PROPERTIES
62 |
63 | func testAbs() {
64 | XCTAssertEqual((-10).abs, 10)
65 | XCTAssertEqual(0.abs, 0)
66 | XCTAssertEqual(10.abs, 10)
67 | }
68 |
69 | func testRandom() {
70 |
71 | let value = 10
72 |
73 | for _ in 0...10 {
74 | let result = value.random()
75 |
76 | XCTAssertTrue(result <= 10 && result >= 0)
77 | }
78 | }
79 |
80 | }
81 |
82 | // MARK: ----- TYPE COERCION
83 |
84 | extension IntConvenienceExtensionsTests {
85 |
86 | func testAsString() {
87 | XCTAssertEqual(1.asString, "1")
88 | XCTAssertEqual((-1).asString, "-1")
89 | }
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
92 | # project specific
93 | Resources/
94 |
95 | # filesystem specific
96 | **/.DS_Store
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/Foundation/Int+AngleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Int+AngleTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 26/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class IntAngleExtensionsTests: XCTestCase {
12 |
13 | let degreesValue: Double = 45
14 | let radiansValue: Double = 2
15 | let inverseValue: Double = 1
16 |
17 | func testIntAsDegree() {
18 | let expectedResult = Angle.degrees(degreesValue)
19 | let result = Int(degreesValue).degree
20 | XCTAssertEqual(result, expectedResult)
21 | }
22 |
23 | func testIntAsDegrees() {
24 | let expectedResult = Angle.degrees(degreesValue)
25 | let result = Int(degreesValue).degrees
26 | XCTAssertEqual(result, expectedResult)
27 | }
28 |
29 | func testIntAsCycles() {
30 | XCTAssertEqual(1.cycle, Angle.degrees(360))
31 | XCTAssertEqual(2.cycles, Angle.degrees(720))
32 | XCTAssertEqual(-1.cycle, Angle.degrees(-360))
33 | XCTAssertEqual(0.cycles, Angle.degrees(0))
34 | }
35 |
36 | func testIntAsRadian() {
37 | let expectedResult = Angle.radians(radiansValue)
38 | let result = Int(radiansValue).radian
39 | XCTAssertEqual(result, expectedResult)
40 | }
41 |
42 | func testIntAsRadians() {
43 | let expectedResult = Angle.radians(radiansValue)
44 | let result = Int(radiansValue).radians
45 | XCTAssertEqual(result, expectedResult)
46 | }
47 |
48 | func testIntDegreesAsRadians() {
49 | let expectedResult = Angle.radians(Angle.degrees(degreesValue).radians)
50 | let result = Int(degreesValue).degreesAsRadians
51 | XCTAssertEqual(result, expectedResult)
52 | }
53 |
54 | func testIntRadiansAsDegrees() {
55 | let expectedResult = Angle.degrees(Angle.radians(radiansValue).degrees)
56 | let result = Int(radiansValue).radiansAsDegrees
57 | XCTAssertEqual(result, expectedResult)
58 | }
59 |
60 | func testIntACos() {
61 | let expectedResult = acos(inverseValue)
62 | let result = Int(inverseValue).acos.radians
63 | XCTAssertEqual(result, expectedResult)
64 | }
65 |
66 | func testIntASin() {
67 | let expectedResult = asin(inverseValue)
68 | let result = Int(inverseValue).asin.radians
69 | XCTAssertEqual(result, expectedResult)
70 | }
71 |
72 | func testIntATan() {
73 | let expectedResult = atan(inverseValue)
74 | let result = Int(inverseValue).atan.radians
75 | XCTAssertEqual(result, expectedResult)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/SwiftUI/GeometryProxy+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GeometryProxy+Convenience.swift
3 | //
4 | // Created by Adam Fordyce on 13/11/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public extension GeometryProxy {
11 |
12 | var width: CGFloat {
13 | size.width
14 | }
15 |
16 | var halfWidth: CGFloat {
17 | size.halfWidth
18 | }
19 |
20 | func widthScaled(_ scale: CGFloat) -> CGFloat {
21 | width * scale
22 | }
23 |
24 | var height: CGFloat {
25 | size.height
26 | }
27 |
28 | var halfHeight: CGFloat {
29 | size.halfHeight
30 | }
31 |
32 | var minDimension: CGFloat {
33 | size.minDimension
34 | }
35 |
36 | func heightScaled(_ scale: CGFloat) -> CGFloat {
37 | height * scale
38 | }
39 |
40 | func sizeScaled(_ scale: CGFloat) -> CGSize {
41 | size.scaled(scale)
42 | }
43 |
44 | func sizeScaled(_ widthScale: CGFloat, _ heightScale: CGFloat) -> CGSize {
45 | size.scaled(widthScale, heightScale)
46 | }
47 |
48 | var localFrame: CGRect {
49 | frame(in: .local)
50 | }
51 |
52 | var localOrigin: CGPoint {
53 | localFrame.origin
54 | }
55 |
56 | var localMinX: CGFloat {
57 | localFrame.minX
58 | }
59 |
60 | var localMinY: CGFloat {
61 | localFrame.minY
62 | }
63 |
64 | var localMidX: CGFloat {
65 | localFrame.midX
66 | }
67 |
68 | var localMidY: CGFloat {
69 | localFrame.midY
70 | }
71 |
72 | var localMaxX: CGFloat {
73 | localFrame.maxX
74 | }
75 |
76 | var localMaxY: CGFloat {
77 | localFrame.maxY
78 | }
79 |
80 | var localCenter: CGPoint {
81 | localFrame.center
82 | }
83 |
84 | var globalFrame: CGRect {
85 | frame(in: .global)
86 | }
87 |
88 | var globalOrigin: CGPoint {
89 | globalFrame.origin
90 | }
91 |
92 | var globalMinX: CGFloat {
93 | globalFrame.minX
94 | }
95 |
96 | var globalMinY: CGFloat {
97 | globalFrame.minY
98 | }
99 |
100 | var globalMidX: CGFloat {
101 | globalFrame.midX
102 | }
103 |
104 | var globalMidY: CGFloat {
105 | globalFrame.midY
106 | }
107 |
108 | var globalMaxX: CGFloat {
109 | globalFrame.maxX
110 | }
111 |
112 | var globalMaxY: CGFloat {
113 | globalFrame.maxY
114 | }
115 |
116 | var globalCenter: CGPoint {
117 | globalFrame.center
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Model/LayoutGuide/View+LayoutGuide.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+GridLayoutHelpers.swift
3 | //
4 | // Created by Adam Fordyce on 03/02/2020.
5 | // Copyright © 2020 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | public extension View {
9 |
10 | func layoutGuide(_ layoutGuideConfig: LayoutGuideConfig, color: Color = .gray) -> some View {
11 | modifier(LayoutViewModifier(layoutGuideConfig: layoutGuideConfig, attributes: LayoutOverlayAttributes(color: color)))
12 | }
13 |
14 | func layoutGuide(_ layoutGuideConfig: LayoutGuideConfig, color: Color = .gray, lineWidth: CGFloat) -> some View {
15 | modifier(LayoutViewModifier(layoutGuideConfig: layoutGuideConfig, attributes: LayoutOverlayAttributes(color: color, lineWidth: lineWidth.asCGFloat)))
16 | }
17 |
18 | func layoutGuide(_ layoutGuideConfig: LayoutGuideConfig, color: Color = .gray, opacity: CGFloat) -> some View {
19 | modifier(LayoutViewModifier(layoutGuideConfig: layoutGuideConfig, attributes: LayoutOverlayAttributes(color: color, opacity: opacity.asDouble)))
20 | }
21 |
22 | func layoutGuide(_ layoutGuideConfig: LayoutGuideConfig, color: Color = .gray, lineWidth: CGFloat, opacity: CGFloat) -> some View {
23 | modifier(LayoutViewModifier(layoutGuideConfig: layoutGuideConfig, attributes: LayoutOverlayAttributes(color: color, lineWidth: lineWidth.asCGFloat, opacity: opacity.asDouble)))
24 | }
25 | }
26 |
27 | private struct LayoutOverlayAttributes {
28 | let color: Color
29 | let lineWidth: CGFloat
30 | let opacity: Double
31 |
32 | init(color: Color = .gray, lineWidth: CGFloat = 0.5, opacity: Double = 0.5) {
33 | self.color = color
34 | self.lineWidth = lineWidth
35 | self.opacity = opacity
36 | }
37 | }
38 |
39 | private struct LayoutViewModifier: ViewModifier {
40 |
41 | private let layoutGuideConfig: LayoutGuideConfig
42 | private let attributes: LayoutOverlayAttributes
43 |
44 | @State private var frame: CGRect = .zero
45 |
46 | @Environment(\.showLayoutGuides) var showLayoutOverlays
47 |
48 | init(layoutGuideConfig: LayoutGuideConfig, attributes: LayoutOverlayAttributes) {
49 | self.layoutGuideConfig = layoutGuideConfig
50 | self.attributes = attributes
51 | }
52 |
53 | @ViewBuilder func body(content: Content) -> some View {
54 | if showLayoutOverlays {
55 | content
56 | .geometryReader { (geo: GeometryProxy) in
57 | self.frame = geo.localFrame
58 | }
59 | .overlay(self.layoutGuideConfig
60 | .stroke(self.attributes.color, lineWidth: self.attributes.lineWidth).opacity(self.attributes.opacity).allowsHitTesting(false))
61 | } else {
62 | content
63 | }
64 | }
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/SwiftUI/Path+LinesAndShapes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Path+LinesAndShapes.swift
3 | //
4 | // Created by Adam Fordyce on 23/08/2021.
5 | // Copyright © 2021 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public typealias PointAndRadius = (point: CGPoint, radius: CGFloat)
11 |
12 | public extension Path {
13 |
14 | mutating func lines(_ points: CGPoint..., cornerRadius: CGFloat = 0) {
15 | lines(points, cornerRadius: cornerRadius)
16 | }
17 |
18 | mutating func lines(_ points: [CGPoint], cornerRadius: CGFloat = 0) {
19 |
20 | for index in 0..<(points.count - 1) {
21 | arc(points[index], points[index + 1], radius: cornerRadius)
22 | }
23 | line(points.last!)
24 | }
25 |
26 | mutating func lines(_ points: PointAndRadius...) {
27 | lines(points)
28 | }
29 |
30 | mutating func lines(_ points: [PointAndRadius]) {
31 | guard points.count > 0 else { return }
32 |
33 | for index in 0..<(points.count - 1) {
34 | arc(points[index].point, points[index + 1].point, radius: points[index].radius)
35 | }
36 | line(points.last?.point ?? .zero)
37 | }
38 |
39 | mutating func shape(_ points: CGPoint..., cornerRadius: CGFloat = 0) {
40 | shape(points, cornerRadius: cornerRadius)
41 | }
42 |
43 | mutating func shape(_ points: [CGPoint], cornerRadius: CGFloat = 0) {
44 |
45 | guard points.count > 0 else { return }
46 |
47 | if points.count == 1 {
48 | line(points.first!)
49 | } else {
50 | let startingPoint = points.first!.to(points.last!, 0.5)
51 |
52 | move(startingPoint)
53 |
54 | for index in 0..<(points.count - 1) {
55 | arc(points[index], points[index + 1], radius: cornerRadius)
56 | }
57 | arc(points.last!, startingPoint, radius: cornerRadius)
58 | line(startingPoint)
59 | }
60 | }
61 |
62 | mutating func shape(_ points: PointAndRadius...) {
63 | shape(points)
64 | }
65 |
66 | mutating func shape(_ points: [PointAndRadius]) {
67 | guard points.count > 0 else { return }
68 |
69 | if points.count == 1 {
70 | line(points.first!.point)
71 | } else {
72 |
73 | let startingPoint = points.first!.point.to(points.last!.point, 0.5)
74 |
75 | move(startingPoint)
76 |
77 | for index in 0..<(points.count - 1) {
78 | arc(points[index].point, points[index + 1].point, radius: points[index].radius)
79 | }
80 | arc(points.last!.point, startingPoint, radius: points.last!.radius)
81 | line(startingPoint)
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/CoreGraphics/CGVector+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGVector+Convenience.swift
3 | //
4 | // Created by Adam Fordyce on 25/10/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | public extension CGVector {
9 |
10 | init(_ size: CGFloat) {
11 | self.init(size, size)
12 | }
13 |
14 | init(_ dx: CGFloat, _ dy: CGFloat) {
15 | self.init(dx: dx, dy: dy)
16 | }
17 |
18 | var asCGRect: CGRect {
19 | CGRect(dx, dy)
20 | }
21 |
22 | var asCGPoint: CGPoint {
23 | CGPoint(dx, dy)
24 | }
25 |
26 | var asCGSize: CGSize {
27 | CGSize(dx, dy)
28 | }
29 |
30 | var asUnitPoint: UnitPoint {
31 | UnitPoint(dx, dy)
32 | }
33 |
34 | var width: CGFloat {
35 | dx
36 | }
37 |
38 | var height: CGFloat {
39 | dy
40 | }
41 |
42 | var midX: CGFloat {
43 | width * 0.5
44 | }
45 |
46 | var midY: CGFloat {
47 | height * 0.5
48 | }
49 |
50 | var halfWidth: CGFloat {
51 | midX
52 | }
53 |
54 | var halfHeight: CGFloat {
55 | midY
56 | }
57 |
58 | var minDimension: CGFloat {
59 | min(dx, dy)
60 | }
61 |
62 | var maxDimension: CGFloat {
63 | max(dx, dy)
64 | }
65 |
66 | func scaled(_ scale: CGFloat) -> CGSize {
67 | scaled(.square(scale))
68 | }
69 |
70 | func scaled(_ scale: CGSize) -> CGSize {
71 | .init(widthScaled(scale.width), heightScaled(scale.height))
72 | }
73 |
74 | func widthScaled(_ scale: CGFloat) -> CGFloat {
75 | dx * scale
76 | }
77 |
78 | func heightScaled(_ scale: CGFloat) -> CGFloat {
79 | dy * scale
80 | }
81 |
82 | func clamped(min: CGFloat, max: CGFloat) -> CGVector {
83 | .init(dx.clamped(min: min, max: max), dy.clamped(min: min, max: max))
84 | }
85 | }
86 |
87 | // MARK: ----- STATIC INITIALISERS
88 |
89 | public extension CGVector {
90 |
91 | static func dx(_ dx: CGFloat) -> CGVector {
92 | .init(dx, 0)
93 | }
94 |
95 | static func dy(_ dy: CGFloat) -> CGVector {
96 | .init(0, dy)
97 | }
98 |
99 | static func vector(_ dx: CGFloat, _ dy: CGFloat) -> CGVector {
100 | .init(dx, dy)
101 | }
102 |
103 | static func vector(_ size: CGFloat) -> CGVector {
104 | .init(size)
105 | }
106 | }
107 |
108 | // MARK: ----- OPERATOR OVERLOADS
109 |
110 | public extension CGVector {
111 |
112 | static func -(lhs: CGVector, rhs: CGVector) -> CGVector {
113 | .init(lhs.dx - rhs.dx, lhs.dy - rhs.dy)
114 | }
115 |
116 | static func +(lhs: CGVector, rhs: CGVector) -> CGVector {
117 | .init(lhs.dx + rhs.dx, lhs.dy + rhs.dy)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/CoreGraphics/CGVector+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGVector+ConvenienceTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 06/02/2020.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class CGVectorConvenienceExtensionsTests: XCTestCase {
12 |
13 | let dx: CGFloat = 4
14 | let dy: CGFloat = 6
15 |
16 | let max: CGFloat = 10
17 | let min: CGFloat = 2
18 |
19 | var halfWidth: CGFloat {
20 | dx * 0.5
21 | }
22 |
23 | var halfHeight: CGFloat {
24 | dy * 0.5
25 | }
26 |
27 | var vector: CGVector {
28 | CGVector(dx: dx, dy: dy)
29 | }
30 | }
31 |
32 | // MARK: ----- INIT
33 |
34 | extension CGVectorConvenienceExtensionsTests {
35 |
36 | func testInit() {
37 | XCTAssertEqual(CGVector(dx, dy), vector)
38 | }
39 | }
40 |
41 | // MARK: ----- STATIC INITIALISERS
42 |
43 | extension CGVectorConvenienceExtensionsTests {
44 |
45 | func testStaticInitialisers() {
46 | XCTAssertEqual(.dx(dx), CGVector(dx, 0))
47 | XCTAssertEqual(.dy(dy), CGVector(0, dy))
48 | XCTAssertEqual(.vector(dx, dy), vector)
49 | XCTAssertEqual(.vector(dx), CGVector(dx, dx))
50 | }
51 | }
52 |
53 | // MARK: ----- TYPE COERCION
54 |
55 | extension CGVectorConvenienceExtensionsTests {
56 |
57 | func testAsType() {
58 | XCTAssertEqual(vector.asCGRect, CGRect(0, 0, dx, dy))
59 | XCTAssertEqual(vector.asCGPoint, CGPoint(dx, dy))
60 | XCTAssertEqual(vector.asCGSize, CGSize(dx, dy))
61 | XCTAssertEqual(vector.asUnitPoint, UnitPoint(dx, dy))
62 | }
63 | }
64 |
65 | // MARK: ----- DIMENSIONS
66 |
67 | extension CGVectorConvenienceExtensionsTests {
68 |
69 | func testDimensions() {
70 | XCTAssertEqual(vector.width, dx)
71 | XCTAssertEqual(vector.height, dy)
72 | XCTAssertEqual(vector.midX, halfWidth)
73 | XCTAssertEqual(vector.midY, halfHeight)
74 | XCTAssertEqual(vector.halfWidth, halfWidth)
75 | XCTAssertEqual(vector.halfHeight, halfHeight)
76 | XCTAssertEqual(vector.maxDimension, dy)
77 | XCTAssertEqual(vector.minDimension, dx)
78 | }
79 | }
80 |
81 | // MARK: ----- SCALED
82 |
83 | extension CGVectorConvenienceExtensionsTests {
84 |
85 | func testScaled() {
86 | XCTAssertEqual(vector.scaled(0.5), CGSize(halfWidth, halfHeight))
87 | XCTAssertEqual(vector.widthScaled(0.5), halfWidth)
88 | XCTAssertEqual(vector.heightScaled(0.5), halfHeight)
89 | }
90 | }
91 |
92 | // MARK: ----- CLAMPING
93 |
94 | extension CGVectorConvenienceExtensionsTests {
95 |
96 | func testClamping() {
97 | XCTAssertEqual(vector.clamped(min: 4.1, max: 5.9), CGVector(4.1, 5.9))
98 | XCTAssertEqual(vector.clamped(min: 4.1, max: 8), CGVector(4.1, dy))
99 | XCTAssertEqual(vector.clamped(min: 2, max: 5.9), CGVector(dx, 5.9))
100 | XCTAssertEqual(vector.clamped(min: 2, max: 10), vector)
101 | }
102 | }
103 |
104 | // MARK: ----- OPERATORS
105 |
106 | extension CGVectorConvenienceExtensionsTests {
107 |
108 | func testMinus() {
109 | XCTAssertEqual(vector - CGVector(1, 1), CGVector(dx - 1, dy - 1))
110 | XCTAssertEqual(vector - vector, .zero)
111 | }
112 |
113 | func testPlus() {
114 | XCTAssertEqual(vector + vector, CGVector(dx + dx, dy + dy))
115 | XCTAssertEqual(vector + CGVector(1, 1), CGVector(dx + 1, dy + 1))
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/SwiftUI/UnitPoint+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnitPoint+ConvenienceTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 06/02/2020.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class UnitPointConvenienceExtensionsTests: XCTestCase {
12 |
13 | let x: CGFloat = 0.5
14 | let y: CGFloat = 0.7
15 | }
16 |
17 | // MARK: ----- INIT
18 |
19 | extension UnitPointConvenienceExtensionsTests {
20 |
21 | func testInit() {
22 | XCTAssertEqual(UnitPoint(x, y), UnitPoint(x: x, y: y))
23 | }
24 | }
25 |
26 | // MARK: ----- COERCION
27 |
28 | extension UnitPointConvenienceExtensionsTests {
29 |
30 | func testAsCGPoint() {
31 | XCTAssertEqual(UnitPoint(0.3, 0.5).asCGPoint, CGPoint(0.3, 0.5))
32 | }
33 | }
34 |
35 | // MARK: ----- AS ANGLE
36 |
37 | extension UnitPointConvenienceExtensionsTests {
38 |
39 | func testAsAngleForNamedUnitPoints() {
40 | XCTAssertEqual(UnitPoint.topLeading.asAngle, .topLeading)
41 | XCTAssertEqual(UnitPoint.top.asAngle, .top)
42 | XCTAssertEqual(UnitPoint.topTrailing.asAngle, .topTrailing)
43 | XCTAssertEqual(UnitPoint.trailing.asAngle, .trailing)
44 | XCTAssertEqual(UnitPoint.bottomTrailing.asAngle, .bottomTrailing)
45 | XCTAssertEqual(UnitPoint.bottom.asAngle, .bottom)
46 | XCTAssertEqual(UnitPoint.bottomLeading.asAngle, .bottomLeading)
47 | XCTAssertEqual(UnitPoint.leading.asAngle, .leading)
48 | }
49 |
50 | func testAsAngle() {
51 | assertEqual(UnitPoint(0.1, 0.1).asAngle.asUnitPoint, Angle.topLeading.asUnitPoint)
52 | assertEqual(UnitPoint(0.5, 0.1).asAngle.degrees, Angle.top.degrees)
53 | assertEqual(UnitPoint(0.9, 0.1).asAngle.degrees, Angle.topTrailing.degrees)
54 | assertEqual(UnitPoint(0.9, 0.5).asAngle.degrees, Angle.trailing.degrees)
55 | assertEqual(UnitPoint(0.9, 0.9).asAngle.degrees, Angle.bottomTrailing.degrees)
56 | assertEqual(UnitPoint(0.5, 0.9).asAngle.degrees, Angle.bottom.degrees)
57 | assertEqual(UnitPoint(0.1, 0.9).asAngle.degrees, Angle.bottomLeading.degrees)
58 | assertEqual(UnitPoint(0, 0.5).asAngle.degrees, Angle.leading.degrees)
59 | }
60 | }
61 |
62 | // MARK: ----- INVERTED
63 |
64 | extension UnitPointConvenienceExtensionsTests {
65 |
66 | func testInvertedForNamedUnitPoints() {
67 | XCTAssertEqual(UnitPoint.topLeading.inverted(), .bottomTrailing)
68 | XCTAssertEqual(UnitPoint.top.inverted(), .bottom)
69 | XCTAssertEqual(UnitPoint.topTrailing.inverted(), .bottomLeading)
70 | XCTAssertEqual(UnitPoint.trailing.inverted(), .leading)
71 | XCTAssertEqual(UnitPoint.bottomTrailing.inverted(), .topLeading)
72 | XCTAssertEqual(UnitPoint.bottom.inverted(), .top)
73 | XCTAssertEqual(UnitPoint.bottomLeading.inverted(), .topTrailing)
74 | XCTAssertEqual(UnitPoint.leading.inverted(), .trailing)
75 | }
76 |
77 | func testInverted() {
78 | assertEqual(UnitPoint(0.1, 0.1).inverted(), UnitPoint(0.9, 0.9))
79 | assertEqual(UnitPoint(0.5, 0.1).inverted(), UnitPoint(0.5, 0.9))
80 | assertEqual(UnitPoint(0.9, 0.1).inverted(), UnitPoint(0.1, 0.9))
81 | assertEqual(UnitPoint(0.9, 0.5).inverted(), UnitPoint(0.1, 0.5))
82 | assertEqual(UnitPoint(0.9, 0.9).inverted(), UnitPoint(0.1, 0.1))
83 | assertEqual(UnitPoint(0.5, 0.9).inverted(), UnitPoint(0.5, 0.1))
84 | assertEqual(UnitPoint(0.1, 0.9).inverted(), UnitPoint(0.9, 0.1))
85 | assertEqual(UnitPoint(0, 0.5).inverted(), UnitPoint(1, 0.5))
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/UIKit/UIScreen+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIScreen+Convenience.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 18/12/2019.
6 | // Copyright © 2019 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | #if canImport(UIKit) && !os(watchOS)
10 | import UIKit
11 |
12 | public extension UIScreen {
13 |
14 | static func mainOrigin() -> CGPoint {
15 | main.origin
16 | }
17 |
18 | static func mainCenter() -> CGPoint {
19 | main.center
20 | }
21 |
22 | static var mainWidth: CGFloat {
23 | main.width
24 | }
25 |
26 | static var halfMainWidth: CGFloat {
27 | main.size.halfWidth
28 | }
29 |
30 | static func mainWidthScaled(_ scale: CGFloat) -> CGFloat {
31 | main.widthScaled(scale)
32 | }
33 |
34 | static var mainHeight: CGFloat {
35 | main.height
36 | }
37 |
38 | static var halfMainHeight: CGFloat {
39 | main.size.halfHeight
40 | }
41 |
42 | static func mainHeightScaled(_ scale: CGFloat) -> CGFloat {
43 | main.heightScaled(scale)
44 | }
45 |
46 | static var mainMidX: CGFloat {
47 | main.midX
48 | }
49 |
50 | static var mainMidY: CGFloat {
51 | main.midY
52 | }
53 |
54 | static var mainMinX: CGFloat {
55 | main.minX
56 | }
57 |
58 | static var mainMinY: CGFloat {
59 | main.minY
60 | }
61 |
62 | static var mainMaxX: CGFloat {
63 | main.maxX
64 | }
65 |
66 | static var mainMaxY: CGFloat {
67 | main.maxY
68 | }
69 |
70 | static func mainSizeScaled(_ scale: CGFloat) -> CGSize {
71 | main.sizeScaled(scale)
72 | }
73 |
74 | static func mainSizeScaled(_ widthScale: CGFloat, _ heightScale: CGFloat) -> CGSize {
75 | main.sizeScaled(widthScale, heightScale)
76 | }
77 |
78 | static var mainSize: CGSize {
79 | main.size
80 | }
81 |
82 | static var mainBounds: CGRect {
83 | main.bounds
84 | }
85 |
86 | var origin: CGPoint {
87 | bounds.origin
88 | }
89 |
90 | var center: CGPoint {
91 | bounds.center
92 | }
93 |
94 | var width: CGFloat {
95 | bounds.width
96 | }
97 |
98 | var halfWidth: CGFloat {
99 | bounds.halfWidth
100 | }
101 |
102 | func widthScaled(_ scale: CGFloat) -> CGFloat {
103 | width * scale
104 | }
105 |
106 | var height: CGFloat {
107 | bounds.height
108 | }
109 |
110 | var halfHeight: CGFloat {
111 | bounds.halfHeight
112 | }
113 |
114 | func heightScaled(_ scale: CGFloat) -> CGFloat {
115 | height * scale
116 | }
117 |
118 | var size: CGSize {
119 | bounds.size
120 | }
121 |
122 | func sizeScaled(_ scale: CGFloat) -> CGSize {
123 | bounds.size.scaled(scale)
124 | }
125 |
126 | func sizeScaled(_ widthScale: CGFloat, _ heightScale: CGFloat) -> CGSize {
127 | bounds.size.scaled(widthScale, heightScale)
128 | }
129 |
130 | var midX: CGFloat {
131 | bounds.midX
132 | }
133 |
134 | var midY: CGFloat {
135 | bounds.midY
136 | }
137 |
138 | var minX: CGFloat {
139 | bounds.origin.x
140 | }
141 |
142 | var minY: CGFloat {
143 | bounds.origin.y
144 | }
145 |
146 | var maxX: CGFloat {
147 | bounds.maxX
148 | }
149 |
150 | var maxY: CGFloat {
151 | bounds.maxY
152 | }
153 | }
154 | #endif
155 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Model/LayoutGuide/LayoutCoordinatorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LayoutCoordinatorTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 14/02/2020.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class LayoutCoordinatorTests: BaseLayoutGuideTests {
12 |
13 | var columns = 10
14 | var rows = 20
15 |
16 | var layoutConfig: LayoutGuideConfig {
17 | return LayoutGuideConfig.grid(columns: columns, rows: rows)
18 | }
19 |
20 | var grid: LayoutGuide {
21 | layoutConfig.layout(in: rect)
22 | }
23 | }
24 |
25 | // MARK: ----- OFFSET LAYOUT COORDINATOR PROPERTIES
26 |
27 | extension LayoutCoordinatorTests {
28 |
29 | func testOffsetLayoutProperties() {
30 | var grid = self.grid
31 | assertEqual(grid[0,0], rect.origin)
32 | grid = grid.offset(.x(10))
33 | assertEqual(grid[0,0], rect.origin.xOffset(10))
34 | grid = grid.offset(.y(20))
35 | assertEqual(grid[0,0], rect.origin.offset(10, 20))
36 |
37 | XCTAssertEqual(grid.xCount, columns)
38 | XCTAssertEqual(grid.yCount, rows)
39 | }
40 | }
41 |
42 | // MARK: ----- REFRAMED OFFSET LAYOUT COORDINATOR
43 |
44 | extension LayoutCoordinatorTests {
45 |
46 | func testReframedOffsetLayoutCoordinator() {
47 | var grid = self.grid
48 | grid = grid.offset(.x(10))
49 | grid = grid.reframed(bottomRightRect, origin: .topLeading)
50 |
51 | assertEqual(grid[10,20], bottomRightRect.bottomTrailing.xOffset(10))
52 | }
53 | }
54 |
55 | // MARK: ----- ROTATED LAYOUT COORDINATOR PROPERTIES
56 |
57 | extension LayoutCoordinatorTests {
58 |
59 | func testRotatedLayoutProperties() {
60 | var grid = self.grid
61 | assertEqual(grid[5, 10], rect.center)
62 | grid = grid.rotated(10.degrees)
63 | assertEqual(grid[5, 10], rect.center)
64 |
65 | XCTAssertEqual(grid.xCount, columns)
66 | XCTAssertEqual(grid.yCount, rows)
67 | }
68 | }
69 |
70 | // MARK: ----- REFRAMED ROTATED LAYOUT COORDINATOR
71 |
72 | extension LayoutCoordinatorTests {
73 |
74 | func testReframedRotatedLayoutCoordinator() {
75 | var grid = self.grid
76 | grid = grid.rotated(90.degrees)
77 | grid = grid.reframed(bottomRightRect, origin: .center)
78 |
79 | assertEqual(grid[0,0], rect.bottomTrailing.offset(bottomRightRect.halfHeight, -bottomRightRect.halfWidth))
80 | }
81 | }
82 |
83 | // MARK: ----- SCALED LAYOUT COORDINATOR PROPERTIES
84 |
85 | extension LayoutCoordinatorTests {
86 |
87 | func testScaledLayoutProperties() {
88 | var grid = self.grid
89 | assertEqual(grid[5, 10], rect.center)
90 | grid = grid.scaled(2)
91 | assertEqual(grid[5, 10], rect.center)
92 |
93 | let location = grid.anchorLocation(for: .top)
94 | XCTAssertEqual(location, rect.top)
95 |
96 | XCTAssertEqual(grid.xCount, columns)
97 | XCTAssertEqual(grid.yCount, rows)
98 | }
99 | }
100 |
101 | // MARK: ----- REFRAMED SCALED LAYOUT COORDINATOR
102 |
103 | extension LayoutCoordinatorTests {
104 |
105 | func testReframedScaledLayoutCoordinator() {
106 | var grid = self.grid
107 | grid = grid.scaled(2)
108 | grid = grid.reframed(bottomRightRect)
109 |
110 | assertEqual(grid[10,20], bottomRightRect.bottomTrailing.offset(bottomRightRect.sizeScaled(0.5)))
111 | assertEqual(grid[5,10], bottomRightRect.center)
112 | assertEqual(grid[0,0], bottomRightRect.topLeading.offset(bottomRightRect.sizeScaled(-0.5)))
113 | }
114 | }
115 |
116 |
117 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/SwiftUI/Angle+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Angle+Convenience.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 20/11/2019.
6 | // Copyright © 2019 Adam Fordyce. All rights reserved.
7 | //
8 | import SwiftUI
9 |
10 | private let offsetFromTopAngle = 90.degrees
11 |
12 | public func offsetAsAngleFromTop(_ angle: Angle) -> Angle {
13 | angle - offsetFromTopAngle
14 | }
15 |
16 | public extension Angle {
17 |
18 | static func *(lhs: Angle, rhs: Double) -> Angle {
19 | Angle(degrees: lhs.degrees * rhs)
20 | }
21 |
22 | static func /(lhs: Angle, rhs: Double) -> Angle {
23 | Angle(degrees: lhs.degrees / rhs)
24 | }
25 |
26 | static func *(lhs: Angle, rhs: CGFloat) -> Angle {
27 | lhs * rhs.asDouble
28 | }
29 |
30 | static func /(lhs: Angle, rhs: CGFloat) -> Angle {
31 | lhs / rhs.asDouble
32 | }
33 |
34 | static func *(lhs: Angle, rhs: Int) -> Angle {
35 | lhs * rhs.asDouble
36 | }
37 |
38 | static func /(lhs: Angle, rhs: Int) -> Angle {
39 | lhs / rhs.asDouble
40 | }
41 | }
42 |
43 | // MARK: ----- PROPERTIES
44 |
45 | public extension Angle {
46 |
47 | var fromTop: Angle {
48 | offsetAsAngleFromTop(self)
49 | }
50 | }
51 |
52 | // MARK: ----- TRIGONOMETRY
53 |
54 | public extension Angle {
55 |
56 | var cos: Double {
57 | Darwin.cos(self.radians)
58 | }
59 |
60 | var sin: Double {
61 | Darwin.sin(self.radians)
62 | }
63 |
64 | var tan: Double {
65 | Darwin.tan(self.radians)
66 | }
67 | }
68 |
69 | // MARK: ----- COORDINATES
70 |
71 | public extension Angle {
72 |
73 | static let topLeading = -135.degrees
74 | static let top = -90.degrees
75 | static let topTrailing = -45.degrees
76 | static let trailing = 0.degrees
77 | static let bottomTrailing = 45.degrees
78 | static let bottom = 90.degrees
79 | static let bottomLeading = 135.degrees
80 | static let leading = 180.degrees
81 | }
82 |
83 | // MARK: ----- CYCLES (MULTIPLES OF ONE ROTATION)
84 |
85 | public extension Angle {
86 |
87 | static func cycles(_ cycles: Double) -> Angle {
88 | (360.0 * cycles).degrees
89 | }
90 |
91 | var cycles: Double {
92 | degrees / 360
93 | }
94 | }
95 |
96 | // MARK: ----- UNIT POINT CONVERSION
97 |
98 | private let maxUnitRadius = sqrt(0.5 * 0.5 + 0.5 * 0.5).asCGFloat
99 |
100 | private let unitPointForNamedAngle: [Angle: UnitPoint] = [
101 |
102 | .topLeading: .topLeading,
103 | .top: .top,
104 | .topTrailing: .topTrailing,
105 | .trailing: .trailing,
106 | .bottomTrailing: .bottomTrailing,
107 | .bottom: .bottom,
108 | .bottomLeading: .bottomLeading,
109 | .leading: .leading,
110 | ]
111 |
112 | public extension Angle {
113 |
114 | var asUnitPoint: UnitPoint {
115 | if let unitPoint = unitPointForNamedAngle[self] {
116 | return unitPoint
117 | } else {
118 | var offset = calcOffset(radius: maxUnitRadius, angle: self)
119 |
120 | if abs(offset.x) > 0.5 {
121 | let ratio = 0.5 / abs(offset.x)
122 | offset = offset.scaled(ratio)
123 | } else if abs(offset.y) > 0.5 {
124 | let ratio = 0.5 / abs(offset.y)
125 | offset = offset.scaled(ratio)
126 | }
127 | return offset.offset(.point(0.5, 0.5)).asUnitPoint
128 | }
129 | }
130 | }
131 |
132 | // MARK: ----- TO WITH FACTOR
133 |
134 | public extension Angle {
135 |
136 | func to(_ destination: Angle, _ factor: CGFloat) -> Angle {
137 | let delta = destination.degrees - self.degrees
138 | return (self.degrees + delta * factor.asDouble).degrees
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/CoreGraphics/CGSize+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGSize+Convenience.swift
3 | //
4 | // Created by Adam Fordyce on 25/10/2019.
5 | // Copyright © 2019 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import CoreGraphics
9 |
10 | public extension CGSize {
11 |
12 | init(_ size: CGFloat) {
13 | self.init(size, size)
14 | }
15 |
16 | init(_ width: CGFloat, _ height: CGFloat) {
17 | self.init(width: width, height: height)
18 | }
19 |
20 | var asCGRect: CGRect {
21 | CGRect(width, height)
22 | }
23 |
24 | var asCGPoint: CGPoint {
25 | CGPoint(width, height)
26 | }
27 |
28 | var asCGVector: CGVector {
29 | CGVector(width, height)
30 | }
31 |
32 | var asUnitPoint: UnitPoint {
33 | UnitPoint(width, height)
34 | }
35 |
36 | var x: CGFloat {
37 | width
38 | }
39 |
40 | var y: CGFloat {
41 | height
42 | }
43 |
44 | var midX: CGFloat {
45 | width * 0.5
46 | }
47 |
48 | var midY: CGFloat {
49 | height * 0.5
50 | }
51 |
52 | var halfWidth: CGFloat {
53 | midX
54 | }
55 |
56 | var halfHeight: CGFloat {
57 | midY
58 | }
59 |
60 | var minDimension: CGFloat {
61 | min(width, height)
62 | }
63 |
64 | var maxDimension: CGFloat {
65 | max(width, height)
66 | }
67 |
68 | func scaled(_ scale: CGFloat) -> CGSize {
69 | scaled(.square(scale))
70 | }
71 |
72 | func scaled(_ scale: CGSize) -> CGSize {
73 | scaled(scale.width, scale.height)
74 | }
75 |
76 | func scaled(_ widthScale: CGFloat, _ heightScale: CGFloat) -> CGSize {
77 | .init(widthScaled(widthScale), heightScaled(heightScale))
78 | }
79 |
80 | func widthScaled(_ scale: CGFloat) -> CGFloat {
81 | width * scale.asCGFloat
82 | }
83 |
84 | func heightScaled(_ scale: CGFloat) -> CGFloat {
85 | height * scale.asCGFloat
86 | }
87 |
88 | func clamped(min: CGFloat, max: CGFloat) -> CGSize {
89 | .init(self.width.clamped(min: min, max: max), self.height.clamped(min: min, max: max))
90 | }
91 | }
92 |
93 | // MARK: ----- OPERATOR OVERLOADS
94 |
95 | public extension CGSize {
96 |
97 | static func -(lhs: CGSize, rhs: CGSize) -> CGSize {
98 | .init(lhs.width - rhs.width, lhs.height - rhs.height)
99 | }
100 |
101 | static func +(lhs: CGSize, rhs: CGSize) -> CGSize {
102 | .init(lhs.width + rhs.width, lhs.height + rhs.height)
103 | }
104 | }
105 |
106 | // MARK: ----- STATIC INITIALISERS
107 |
108 | public extension CGSize {
109 |
110 | static func square(_ size: CGFloat) -> CGSize {
111 | .init(size)
112 | }
113 |
114 | static func width(_ width: CGFloat) -> CGSize {
115 | .init(width, 0)
116 | }
117 |
118 | static func height(_ height: CGFloat) -> CGSize {
119 | .init(0, height)
120 | }
121 |
122 | static func size(_ width: CGFloat, _ height: CGFloat) -> CGSize {
123 | .init(width, height)
124 | }
125 |
126 | static func size(_ size: CGFloat) -> CGSize {
127 | .square(size)
128 | }
129 | }
130 |
131 | // MARK: ----- MAP FROM ONE RECT TO ANOTHER
132 |
133 | public extension CGSize {
134 |
135 | func map(from: CGRect, to: CGRect) -> CGSize {
136 | let relativeOffsetWidth = from.width == 0 ? width : width / from.width
137 | let relativeOffsetHeight = from.height == 0 ? height : height / from.height
138 |
139 | return CGSize(relativeOffsetWidth * to.width, relativeOffsetHeight * to.height)
140 | }
141 | }
142 |
143 | // MARK: ----- TO WITH FACTOR
144 |
145 | public extension CGSize {
146 |
147 | func to(_ destination: CGSize, _ factor: CGFloat) -> CGSize {
148 | to(destination, .square(factor))
149 | }
150 |
151 | func to(_ destination: CGSize, _ factor: CGSize) -> CGSize {
152 | let deltaX = destination.x - self.x
153 | let deltaY = destination.y - self.y
154 | return CGSize(self.x + deltaX * factor.x, self.y + deltaY * factor.y)
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/SwiftUI/Angle+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Angle+ConvenienceTests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 27/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class AngleConvenienceExtensionsTests: XCTestCase {
12 |
13 | func testOperatorMultiply() {
14 | XCTAssertEqual(4.degrees, 2.degrees * 2)
15 | }
16 |
17 | func testOperatorDivide() {
18 | XCTAssertEqual(2.degrees, 4.degrees / 2)
19 | }
20 |
21 | func testCos() {
22 | XCTAssertEqual(2.degrees.cos, cos(Angle(degrees: 2.0).radians))
23 | }
24 |
25 | func testSin() {
26 | XCTAssertEqual(2.degrees.sin, sin(Angle(degrees: 2.0).radians))
27 | }
28 |
29 | func testTan() {
30 | XCTAssertEqual(2.degrees.tan, tan(Angle(degrees: 2.0).radians))
31 | }
32 | }
33 |
34 | // MARK: ----- STATIC CONSTANTS
35 |
36 | extension AngleConvenienceExtensionsTests {
37 |
38 | func testStaticConstants() {
39 | XCTAssertEqual(Angle.topLeading, -135.degrees)
40 | XCTAssertEqual(Angle.top, -90.degrees)
41 | XCTAssertEqual(Angle.topTrailing, -45.degrees)
42 | XCTAssertEqual(Angle.trailing, 0.degrees)
43 | XCTAssertEqual(Angle.bottomTrailing, 45.degrees)
44 | XCTAssertEqual(Angle.bottom, 90.degrees)
45 | XCTAssertEqual(Angle.bottomLeading, 135.degrees)
46 | XCTAssertEqual(Angle.leading, 180.degrees)
47 | }
48 | }
49 |
50 | // MARK: ----- CYCLE
51 |
52 | extension AngleConvenienceExtensionsTests {
53 |
54 | func testCycles() {
55 | XCTAssertEqual(Angle.cycles(0.25), 90.degrees)
56 | XCTAssertEqual(Angle.cycles(0.5), 180.degrees)
57 | XCTAssertEqual(Angle.cycles(0.75), 270.degrees)
58 | }
59 |
60 | func testCyclesProperty() {
61 | XCTAssertEqual(Angle.cycles(0.25).cycles, 0.25)
62 | XCTAssertEqual(Angle.cycles(0.5).cycles, 0.5)
63 | XCTAssertEqual(Angle.cycles(0.75).cycles, 0.75)
64 | XCTAssertEqual(Angle.cycles(-0.75).cycles, -0.75)
65 | }
66 | }
67 |
68 | // MARK: ----- AS UNIT POINT
69 |
70 | extension AngleConvenienceExtensionsTests {
71 |
72 | func testAsUnitPointForNamedAngles() {
73 | XCTAssertEqual(Angle.topLeading.asUnitPoint, .topLeading)
74 | XCTAssertEqual(Angle.top.asUnitPoint, .top)
75 | XCTAssertEqual(Angle.topTrailing.asUnitPoint, .topTrailing)
76 | XCTAssertEqual(Angle.trailing.asUnitPoint, .trailing)
77 | XCTAssertEqual(Angle.bottomTrailing.asUnitPoint, .bottomTrailing)
78 | XCTAssertEqual(Angle.bottom.asUnitPoint, .bottom)
79 | XCTAssertEqual(Angle.bottomLeading.asUnitPoint, .bottomLeading)
80 | XCTAssertEqual(Angle.leading.asUnitPoint, .leading)
81 | }
82 |
83 | func testAsUnitPoint() {
84 | assertEqual((Angle.topLeading + 360.degrees).asUnitPoint, .topLeading)
85 | assertEqual((Angle.top + 360.degrees).asUnitPoint, .top)
86 | assertEqual((Angle.topTrailing + 360.degrees).asUnitPoint, .topTrailing)
87 | assertEqual((Angle.trailing + 360.degrees).asUnitPoint, .trailing)
88 | assertEqual((Angle.bottomTrailing + 360.degrees).asUnitPoint, .bottomTrailing)
89 | assertEqual((Angle.bottom + 360.degrees).asUnitPoint, .bottom)
90 | assertEqual((Angle.bottomLeading + 360.degrees).asUnitPoint, .bottomLeading)
91 | assertEqual((Angle.leading + 360.degrees).asUnitPoint, .leading)
92 | }
93 | }
94 |
95 | // MARK: ----- TO
96 |
97 | extension AngleConvenienceExtensionsTests {
98 |
99 | func testToAngleInDegreesWithFactor() {
100 | let angeFrom = 5.degrees
101 | let angleTo = 10.degrees
102 | XCTAssertEqual(angeFrom.to(angleTo, 0), 5.degrees)
103 | XCTAssertEqual(angeFrom.to(angleTo, 0.5), 7.5.degrees)
104 | XCTAssertEqual(angeFrom.to(angleTo, 1), 10.degrees)
105 | }
106 |
107 | func testToAngleInRadiansWithFactor() {
108 | let angeFrom = 5.radians
109 | let angleTo = 10.radians
110 | XCTAssertEqual(angeFrom.to(angleTo, 0), 5.radians)
111 | XCTAssertEqual(angeFrom.to(angleTo, 0.5), 7.5.radians)
112 | XCTAssertEqual(angeFrom.to(angleTo, 1), 10.radians)
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/CoreGraphics/CGSize+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGSize+Tests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 25/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class CGSizeConvenienceExtensionsTests: XCTestCase {
12 |
13 | let width: CGFloat = 4
14 | let height: CGFloat = 6
15 |
16 | let max: CGFloat = 10
17 | let min: CGFloat = 2
18 |
19 | var halfWidth: CGFloat {
20 | width * 0.5
21 | }
22 |
23 | var halfHeight: CGFloat {
24 | height * 0.5
25 | }
26 |
27 | var size: CGSize {
28 | CGSize(width: width, height: height)
29 | }
30 | }
31 |
32 | // MARK: ----- INIT
33 |
34 | extension CGSizeConvenienceExtensionsTests {
35 |
36 | func testInit() {
37 | XCTAssertEqual(CGSize(width, height), size)
38 | XCTAssertEqual(CGSize(width), CGSize(width, width))
39 | }
40 | }
41 |
42 | // MARK: ----- STATIC INITIALISERS
43 |
44 | extension CGSizeConvenienceExtensionsTests {
45 |
46 | func testStaticInit() {
47 | XCTAssertEqual(CGSize.width(width), CGSize(width, 0))
48 | XCTAssertEqual(CGSize.height(height), CGSize(0, height))
49 | XCTAssertEqual(CGSize.size(width, height), CGSize(width, height))
50 | XCTAssertEqual(CGSize.size(width), CGSize(width, width))
51 | XCTAssertEqual(CGSize.square(width), CGSize(width, width))
52 | }
53 | }
54 |
55 | // MARK: ----- TYPE COERCION
56 |
57 | extension CGSizeConvenienceExtensionsTests {
58 |
59 | func testAsType() {
60 | XCTAssertEqual(size.asCGRect, CGRect(0, 0, width, height))
61 | XCTAssertEqual(size.asCGPoint, CGPoint(width, height))
62 | XCTAssertEqual(size.asCGVector, CGVector(width, height))
63 | XCTAssertEqual(size.asUnitPoint, UnitPoint(width, height))
64 | }
65 | }
66 |
67 | // MARK: ----- DIMENSIONS
68 |
69 | extension CGSizeConvenienceExtensionsTests {
70 |
71 | func testDimensions() {
72 | XCTAssertEqual(size.x, width)
73 | XCTAssertEqual(size.y, height)
74 | XCTAssertEqual(size.midX, halfWidth)
75 | XCTAssertEqual(size.midY, halfHeight)
76 | XCTAssertEqual(size.halfWidth, halfWidth)
77 | XCTAssertEqual(size.halfHeight, halfHeight)
78 | }
79 | }
80 |
81 | // MARK: ----- SCALED
82 |
83 | extension CGSizeConvenienceExtensionsTests {
84 |
85 | func testScaled() {
86 | XCTAssertEqual(size.scaled(0.5), CGSize(halfWidth, halfHeight))
87 | XCTAssertEqual(size.scaled(0.2, 0.7), CGSize(width * 0.2, height * 0.7))
88 | XCTAssertEqual(size.widthScaled(0.5), halfWidth)
89 | XCTAssertEqual(size.heightScaled(0.5), halfHeight)
90 | }
91 | }
92 |
93 | // MARK: ----- CLAMPING
94 |
95 | extension CGSizeConvenienceExtensionsTests {
96 |
97 | func testClamping() {
98 | XCTAssertEqual(size.clamped(min: 4.1, max: 5.9), CGSize(4.1, 5.9))
99 | XCTAssertEqual(size.clamped(min: 4.1, max: 8), CGSize(4.1, height))
100 | XCTAssertEqual(size.clamped(min: 2, max: 5.9), CGSize(width, 5.9))
101 | XCTAssertEqual(size.clamped(min: 2, max: 10), size)
102 | }
103 | }
104 |
105 | // MARK: ----- OPERATORS
106 |
107 | extension CGSizeConvenienceExtensionsTests {
108 |
109 | func testMinus() {
110 | XCTAssertEqual(size - .square(1), CGSize(width - 1, height - 1))
111 | XCTAssertEqual(size - size, .zero)
112 | }
113 |
114 | func testPlus() {
115 | XCTAssertEqual(size + size, CGSize(width + width, height + height))
116 | XCTAssertEqual(size + .square(1), CGSize(width + 1, height + 1))
117 | }
118 | }
119 |
120 | // MARK: ----- STATIC CONSTRUCTORS
121 |
122 | extension CGSizeConvenienceExtensionsTests {
123 |
124 | func testSquare() {
125 | XCTAssertEqual(CGSize.square(width), CGSize(width))
126 | }
127 | }
128 |
129 | // MARK: ----- MAP FROM ONE RECT TO ANOTHER
130 |
131 | extension CGSizeConvenienceExtensionsTests {
132 |
133 | func testMapForSize() {
134 | let sourceRect = CGRect(40, 40, width, height)
135 | let destination = CGRect(0, 0, 15, 10)
136 | XCTAssertEqual(sourceRect.size.map(from: sourceRect, to: destination), destination.size)
137 | assertEqual(sourceRect.sizeScaled(0.4).map(from: sourceRect, to: destination), destination.sizeScaled(0.4))
138 | }
139 | }
140 |
141 | // MARK: ----- TO
142 |
143 | extension CGSizeConvenienceExtensionsTests {
144 |
145 | func testToWithFactor() {
146 | let valueFrom = CGSize(5, 10)
147 | let valueTo = CGSize(10, 20)
148 | XCTAssertEqual(valueFrom.to(valueTo, 0), .size(5, 10))
149 | XCTAssertEqual(valueFrom.to(valueTo, 0.5), .size(7.5, 15))
150 | XCTAssertEqual(valueFrom.to(valueTo, 1), .size(10, 20))
151 |
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Model/LayoutGuide/LayoutCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LayoutCoordinator.swift
3 | //
4 | // Created by Adam Fordyce on 11/02/2020.
5 | // Copyright © 2020 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // MARK: ----- LAYOUT COORDINATORS
11 |
12 | internal protocol LayoutCoordinator {
13 |
14 | subscript(x: Int, y: Int) -> CGPoint {get}
15 | subscript(rel x: CGFloat, y: Int) -> CGPoint {get}
16 | subscript(x: Int, rel y: CGFloat) -> CGPoint {get}
17 | subscript(rel x: CGFloat, rel y: CGFloat) -> CGPoint {get}
18 | func reframed(into rect: CGRect, originalRect: CGRect, origin: UnitPoint?) -> LayoutCoordinator
19 | var xCount: Int {get}
20 | var yCount: Int {get}
21 | var baseOrigin: CGPoint {get}
22 | var baseRect: CGRect {get}
23 | func anchorLocation(for anchor: UnitPoint, size: CGSize) -> CGPoint
24 | var topLeading: CGPoint {get}
25 | var top: CGPoint {get}
26 | var topTrailing: CGPoint {get}
27 | var trailing: CGPoint {get}
28 | var bottomTrailing: CGPoint {get}
29 | var bottom: CGPoint {get}
30 | var bottomLeading: CGPoint {get}
31 | var leading: CGPoint {get}
32 | var center: CGPoint {get}
33 | }
34 |
35 | // MARK: ----- DECORATING LAYOUT COORDINATOR
36 |
37 | internal protocol DecoratingLayoutCoordinator: LayoutCoordinator {
38 | var baseCoordinator: LayoutCoordinator {get}
39 | func transform(_ point: CGPoint) -> CGPoint
40 | }
41 |
42 | internal extension DecoratingLayoutCoordinator {
43 |
44 | var baseRect: CGRect {
45 | baseCoordinator.baseRect
46 | }
47 |
48 | var baseOrigin: CGPoint {
49 | baseCoordinator.baseOrigin
50 | }
51 |
52 | var topLeading: CGPoint {
53 | transform(baseCoordinator.topLeading)
54 | }
55 |
56 | var top: CGPoint {
57 | transform(baseCoordinator.top)
58 | }
59 |
60 | var topTrailing: CGPoint {
61 | transform(baseCoordinator.topTrailing)
62 | }
63 |
64 | var trailing: CGPoint {
65 | transform(baseCoordinator.trailing)
66 | }
67 |
68 | var bottomTrailing: CGPoint {
69 | transform(baseCoordinator.bottomTrailing)
70 | }
71 |
72 | var bottom: CGPoint {
73 | transform(baseCoordinator.bottom)
74 | }
75 |
76 | var bottomLeading: CGPoint {
77 | transform(baseCoordinator.bottomLeading)
78 | }
79 |
80 | var leading: CGPoint {
81 | transform(baseCoordinator.leading)
82 | }
83 |
84 | var center: CGPoint {
85 | transform(baseCoordinator.center)
86 | }
87 |
88 | func anchorLocation(for anchor: UnitPoint, size: CGSize) -> CGPoint {
89 | baseCoordinator.anchorLocation(for: anchor, size: size)
90 | }
91 |
92 | var xCount: Int {
93 | baseCoordinator.xCount
94 | }
95 |
96 | var yCount: Int {
97 | baseCoordinator.yCount
98 | }
99 |
100 | subscript(x: Int, y: Int) -> CGPoint {
101 | transform(self.baseCoordinator[x, y])
102 | }
103 |
104 | subscript(rel x: CGFloat, y: Int) -> CGPoint {
105 | transform(self.baseCoordinator[rel: x, y])
106 | }
107 |
108 | subscript(x: Int, rel y: CGFloat) -> CGPoint {
109 | transform(self.baseCoordinator[x, rel: y])
110 | }
111 |
112 | subscript(rel x: CGFloat, rel y: CGFloat) -> CGPoint {
113 | transform(self.baseCoordinator[rel: x, rel: y])
114 | }
115 | }
116 |
117 | // MARK: ----- ROTATED LAYOUT COORDINATOR
118 |
119 | internal struct RotatedLayoutCoordinator: DecoratingLayoutCoordinator {
120 |
121 | let angle: Angle
122 | let anchor: UnitPoint
123 | let anchorPoint: CGPoint
124 | let baseCoordinator: LayoutCoordinator
125 |
126 | init(angle: Angle, anchor: UnitPoint, anchorPoint: CGPoint, baseCoordinator: LayoutCoordinator) {
127 | self.angle = angle
128 | self.anchor = anchor
129 | self.anchorPoint = anchorPoint
130 | self.baseCoordinator = baseCoordinator
131 | }
132 |
133 | func transform(_ point: CGPoint) -> CGPoint {
134 | let radiusToPoint = anchorPoint.radiusTo(point)
135 | let angleToPoint = anchorPoint.angleTo(point)
136 | let resultAngle = angleToPoint + angle
137 | return anchorPoint.offset(radius: radiusToPoint, angle: resultAngle)
138 | }
139 |
140 | func reframed(into rect: CGRect, originalRect: CGRect, origin: UnitPoint?) -> LayoutCoordinator {
141 |
142 | let reframedBaseCoordinator = baseCoordinator.reframed(into: rect, originalRect: originalRect, origin: origin)
143 |
144 | let reframedRect = reframedBaseCoordinator.baseRect
145 |
146 | let anchorPoint = reframedRect.origin.moveOrigin(in: reframedRect.size, origin: anchor)
147 |
148 | return RotatedLayoutCoordinator(angle: angle, anchor: anchor, anchorPoint: anchorPoint, baseCoordinator: reframedBaseCoordinator)
149 | }
150 | }
151 |
152 | // MARK: ----- OFFSET LAYOUT COORDINATOR
153 |
154 | internal struct OffsetLayoutCoordinator: DecoratingLayoutCoordinator {
155 |
156 | let offset: CGPoint
157 | let baseCoordinator: LayoutCoordinator
158 |
159 | init(offset: CGPoint, baseCoordinator: LayoutCoordinator) {
160 | self.offset = offset
161 | self.baseCoordinator = baseCoordinator
162 | }
163 |
164 | func transform(_ point: CGPoint) -> CGPoint {
165 | point.offset(offset)
166 | }
167 |
168 | func reframed(into rect: CGRect, originalRect: CGRect, origin: UnitPoint? = nil) -> LayoutCoordinator {
169 | return OffsetLayoutCoordinator(offset: offset, baseCoordinator: baseCoordinator.reframed(into: rect, originalRect: originalRect, origin: origin))
170 | }
171 | }
172 |
173 | // MARK: ----- SCALED LAYOUT COORDINATOR
174 |
175 | internal struct ScaledLayoutCoordinator: DecoratingLayoutCoordinator {
176 |
177 | let scale: CGSize
178 | let anchor: UnitPoint
179 | let anchorPoint: CGPoint
180 | let baseCoordinator: LayoutCoordinator
181 |
182 | init(scale: CGSize, anchor: UnitPoint, anchorPoint: CGPoint, baseCoordinator: LayoutCoordinator) {
183 | self.scale = scale
184 | self.anchor = anchor
185 | self.anchorPoint = anchorPoint
186 | self.baseCoordinator = baseCoordinator
187 | }
188 |
189 | func transform(_ point: CGPoint) -> CGPoint {
190 | let offset = point - anchorPoint
191 | return anchorPoint + offset.scaled(scale)
192 | }
193 |
194 | func reframed(into rect: CGRect, originalRect: CGRect, origin: UnitPoint? = nil) -> LayoutCoordinator {
195 |
196 | let reframedBaseCoordinator = baseCoordinator.reframed(into: rect, originalRect: originalRect, origin: origin)
197 | let reframedRect = reframedBaseCoordinator.baseRect
198 | let anchorPoint = reframedRect.origin.moveOrigin(in: reframedRect.size, origin: anchor)
199 |
200 | return ScaledLayoutCoordinator(scale: scale, anchor: anchor, anchorPoint: anchorPoint, baseCoordinator: reframedBaseCoordinator)
201 | }
202 | }
203 |
204 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Model/LayoutGuide/GridLayoutCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GridLayoutCoordinator.swift
3 | //
4 | // Created by Adam Fordyce on 03/02/2020.
5 | // Copyright © 2020 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | private struct GridLayoutCoordinator: LayoutCoordinator {
9 |
10 | let baseOrigin: CGPoint
11 | let baseRect: CGRect
12 | let xOffsetCalculator: OffsetForIndexCalculator
13 | let yOffsetCalculator: OffsetForIndexCalculator
14 |
15 | var xCount: Int {
16 | xOffsetCalculator.indexCount
17 | }
18 |
19 | var yCount: Int {
20 | yOffsetCalculator.indexCount
21 | }
22 |
23 | subscript(x: Int, y: Int) -> CGPoint {
24 | baseOrigin.offset(CGPoint(x: xOffsetCalculator.offsetFor(index: x), y: yOffsetCalculator.offsetFor(index: y)))
25 | }
26 |
27 | subscript(rel x: CGFloat, y: Int) -> CGPoint {
28 | baseOrigin.offset(CGPoint(x: xOffsetCalculator.offsetFor(relativeOffset: x), y: yOffsetCalculator.offsetFor(index: y)))
29 | }
30 |
31 | subscript(x: Int, rel y: CGFloat) -> CGPoint {
32 | baseOrigin.offset(CGPoint(x: xOffsetCalculator.offsetFor(index: x), y: yOffsetCalculator.offsetFor(relativeOffset: y)))
33 | }
34 |
35 | subscript(rel x: CGFloat, rel y: CGFloat) -> CGPoint {
36 | baseOrigin.offset(CGPoint(x: xOffsetCalculator.offsetFor(relativeOffset: x), y: yOffsetCalculator.offsetFor(relativeOffset: y)))
37 | }
38 |
39 | func reframed(into rect: CGRect, originalRect: CGRect, origin: UnitPoint? = nil) -> LayoutCoordinator {
40 |
41 | // let newOrigin = origin == nil ? rect.origin.map(from: originalRect, to: rect) : calcOrigin(in: rect, origin: origin!)
42 | let newOrigin = calcOrigin(in: rect, origin: origin ?? .topLeading)
43 |
44 | let actualRect = CGRect(newOrigin, rect.size)
45 | return GridLayoutCoordinator(
46 | baseOrigin: newOrigin,
47 | baseRect: actualRect,
48 | xOffsetCalculator: xOffsetCalculator.reframed(rect.width),
49 | yOffsetCalculator: yOffsetCalculator.reframed(rect.height))
50 | }
51 |
52 | func anchorLocation(for anchor: UnitPoint, size: CGSize) -> CGPoint {
53 | if anchor == .topLeading {
54 | return baseOrigin
55 | } else {
56 | return baseOrigin.moveOrigin(in: size, origin: anchor)
57 | }
58 | }
59 | }
60 |
61 | private extension GridLayoutCoordinator {
62 |
63 | var topLeading: CGPoint {
64 | baseRect.topLeading
65 | }
66 |
67 | var top: CGPoint {
68 | baseRect.top
69 | }
70 |
71 | var topTrailing: CGPoint {
72 | baseRect.topTrailing
73 | }
74 |
75 | var trailing: CGPoint {
76 | baseRect.trailing
77 | }
78 |
79 | var bottomTrailing: CGPoint {
80 | baseRect.bottomTrailing
81 | }
82 |
83 | var bottom: CGPoint {
84 | baseRect.bottom
85 | }
86 |
87 | var bottomLeading: CGPoint {
88 | baseRect.bottomLeading
89 | }
90 |
91 | var leading: CGPoint {
92 | baseRect.leading
93 | }
94 |
95 | var center: CGPoint {
96 | baseRect.center
97 | }
98 | }
99 |
100 | // MARK: ----- GRID CALCULATOR PROTOCOLS
101 |
102 | private protocol OffsetForIndexCalculator {
103 |
104 | var size: CGFloat {get}
105 | var indexCount: Int {get}
106 | func offsetFor(index: Int) -> CGFloat
107 | func reframed(_ size: CGFloat) -> OffsetForIndexCalculator
108 | }
109 |
110 | private extension OffsetForIndexCalculator {
111 |
112 | func offsetFor(relativeOffset: CGFloat) -> CGFloat {
113 | size * relativeOffset
114 | }
115 | }
116 |
117 | // MARK: ----- EQUIDISTANT OFFSET FOR INDEX
118 |
119 | private struct EquidistantOffsetForIndexCalculator: OffsetForIndexCalculator {
120 |
121 | private let numSlices: Int
122 | private let offsetPerIndex: CGFloat
123 | fileprivate let size: CGFloat
124 |
125 | init(_ size: CGFloat, numSlices: Int) {
126 | self.numSlices = numSlices > 0 ? numSlices : 1
127 | self.offsetPerIndex = size / numSlices.asCGFloat
128 | self.size = size
129 | }
130 |
131 | var indexCount: Int {
132 | numSlices
133 | }
134 |
135 | func offsetFor(index: Int) -> CGFloat {
136 | offsetPerIndex * index.asCGFloat
137 | }
138 |
139 | func reframed(_ size: CGFloat) -> OffsetForIndexCalculator {
140 | EquidistantOffsetForIndexCalculator(size, numSlices: numSlices)
141 | }
142 | }
143 |
144 | // MARK: ----- RELATIVE OFFSET FOR INDEX
145 |
146 | private struct RelativeOffsetForIndexCalculator: OffsetForIndexCalculator {
147 |
148 | private let slices: [CGFloat]
149 | private let offsetSteps: [CGFloat]
150 | fileprivate let size: CGFloat
151 |
152 | init(_ size: CGFloat, slices: [CGFloat]) {
153 |
154 | var offsetSteps: [CGFloat] = []
155 | for slice in slices {
156 | offsetSteps.append(slice.asCGFloat)
157 | }
158 | self.slices = slices
159 | self.offsetSteps = offsetSteps
160 | self.size = size
161 | }
162 |
163 | var indexCount: Int {
164 | slices.count
165 | }
166 |
167 | func offsetFor(index: Int) -> CGFloat {
168 | if index >= 0 && index < offsetSteps.count {
169 | return size * offsetSteps[index]
170 | } else if index >= offsetSteps.count {
171 | return size * (offsetSteps.last ?? 0)
172 | } else {
173 | return size * (offsetSteps.first ?? 0)
174 | }
175 | }
176 |
177 | func reframed(_ size: CGFloat) -> OffsetForIndexCalculator {
178 | RelativeOffsetForIndexCalculator(size, slices: slices)
179 | }
180 | }
181 |
182 | // MARK: ----- LAYOUT EXTENSIONS FOR GRID
183 |
184 | public extension LayoutGuide {
185 |
186 | private static func gridLayout(xOffsetCalculator: OffsetForIndexCalculator, yOffsetCalculator: OffsetForIndexCalculator, rect: CGRect, origin: UnitPoint) -> LayoutGuide {
187 |
188 | let coordinator = GridLayoutCoordinator(
189 | baseOrigin: calcOrigin(in: rect, origin: origin),
190 | baseRect: rect,
191 | xOffsetCalculator: xOffsetCalculator,
192 | yOffsetCalculator: yOffsetCalculator)
193 |
194 | return LayoutGuide(coordinator, rect: rect)
195 | }
196 |
197 | /**
198 | Equidistant x and y
199 | */
200 | static func grid(_ rect: CGRect, columns: Int, rows: Int, origin: UnitPoint = .topLeading) -> LayoutGuide {
201 |
202 | let xOffsetCalculator = EquidistantOffsetForIndexCalculator(rect.width, numSlices: columns)
203 | let yOffsetCalculator = EquidistantOffsetForIndexCalculator(rect.height, numSlices: rows)
204 |
205 | return gridLayout(xOffsetCalculator: xOffsetCalculator, yOffsetCalculator: yOffsetCalculator, rect: rect, origin: origin)
206 | }
207 |
208 | /**
209 | Relative x and equidistant y
210 | */
211 | static func grid(_ rect: CGRect, columns: [CGFloat], rows: Int, origin: UnitPoint = .topLeading) -> LayoutGuide {
212 | let xOffsetCalculator = RelativeOffsetForIndexCalculator(rect.width, slices: columns)
213 | let yOffsetCalculator = EquidistantOffsetForIndexCalculator(rect.height, numSlices: rows)
214 |
215 | return gridLayout(xOffsetCalculator: xOffsetCalculator, yOffsetCalculator: yOffsetCalculator, rect: rect, origin: origin)
216 | }
217 |
218 | /**
219 | Equidistant x and relative y
220 | */
221 | static func grid(_ rect: CGRect, columns: Int, rows: [CGFloat], origin: UnitPoint = .topLeading) -> LayoutGuide {
222 |
223 | let xOffsetCalculator = EquidistantOffsetForIndexCalculator(rect.width, numSlices: columns)
224 | let yOffsetCalculator = RelativeOffsetForIndexCalculator(rect.height, slices: rows)
225 |
226 | return gridLayout(xOffsetCalculator: xOffsetCalculator, yOffsetCalculator: yOffsetCalculator, rect: rect, origin: origin)
227 | }
228 |
229 | /**
230 | Relative x and y
231 | */
232 | static func grid(_ rect: CGRect, columns: [CGFloat], rows: [CGFloat], origin: UnitPoint = .topLeading) -> LayoutGuide {
233 |
234 | let xOffsetCalculator = RelativeOffsetForIndexCalculator(rect.width, slices: columns)
235 | let yOffsetCalculator = RelativeOffsetForIndexCalculator(rect.height, slices: rows)
236 |
237 | return gridLayout(xOffsetCalculator: xOffsetCalculator, yOffsetCalculator: yOffsetCalculator, rect: rect, origin: origin)
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | [PureSwiftUIDesign][pure-swift-ui-design] is a Swift package that brings joy to the process of creating designs using paths in [SwiftUI][swift-ui] code.
8 |
9 | - [Motivation](#motivation)
10 | - [Layout Guides](#layout-guides)
11 | - [Extensions](#extensions)
12 | - [Caveats](#caveats)
13 | - [Installation](#installation)
14 | - [Versioning](#versioning)
15 | - [Version History](#version-history)
16 | - [Licensing](#licensing)
17 | - [Contact](#contact)
18 |
19 | ## Motivation
20 |
21 | Creating paths in [SwiftUI][swift-ui] 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.
22 |
23 | [PureSwiftUIDesign][pure-swift-ui-design] allows you to create incredibly complex and even animated designs quickly, while keeping the code simple.
24 |
25 | It is my hope that the ease with which you can construct shapes using [PureSwiftUIDesign][pure-swift-ui-design]'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.
26 |
27 | There are two main aspects to the package: [layout Guides][docs-layout-guides] and a multitude of extensions all designed to support the act of drawing shapes.
28 |
29 | ## Layout Guides
30 |
31 | [Layout Guides][docs-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.
32 |
33 | In fact, the original logo for [PureSwiftUIDesign][pure-swift-ui-design] 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][gist-pure-swift-ui-design-logo].
34 |
35 | For something a bit similar, consider the following code that draws six pointed star:
36 |
37 | ```swift
38 | struct StarShapeNative: Shape {
39 | func path(in rect: CGRect) -> Path {
40 | Path { path in
41 | let numSegments = 12
42 | let outerRadius = min(rect.height, rect.width) / 2
43 | let innerRadius = outerRadius * innerRadiusRatio
44 | let center = CGPoint(x: rect.midX, y: rect.midY)
45 | let stepAngle = 2 * .pi / CGFloat(numSegments)
46 |
47 | path.move(to: CGPoint(x: rect.midX, y: rect.minY))
48 |
49 | for index in 0.. Path {
73 | Path { path in
74 | let g = starLayoutConfig.layout(in: rect)
75 |
76 | path.move(g[1, 0]) // move to polar coordinate [1, 0] denoting the ring and segment
77 |
78 | for segment in 1..
92 |
94 |
95 |
96 | There is a whole lot more to [layout guides][docs-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][docs-layout-guides] for all the details.
97 |
98 | ## Extensions
99 |
100 | [PureSwiftUIDesign][pure-swift-ui-design] 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.
101 |
102 | Read about all the available extensions and utilities [here][docs-extensions].
103 |
104 | ## Caveats
105 |
106 | [PureSwiftUIDesign][pure-swift-ui-design] defines angles as starting from the trailing edge (as per native SwiftUI) and increasing in a clockwise direction. [Layout guides][docs-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:
107 |
108 | ```swift
109 | let layoutConfig = LayoutGuideConfig.polar(rings: 2, segments: 8, fromTop: false)
110 | ```
111 |
112 | 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:
113 |
114 | ```swift
115 | let layoutConfig = LayoutGuideConfig.polar(rings: 2, segments: [.trailing, .bottom, .leading], fromTop: false)
116 | ```
117 | you *must* remember to set `fromTop` to `false` or the resulting angles will be offset 90 degrees anti-clockwise.
118 |
119 | ## Installation
120 |
121 | The `pure-swift-ui-design` package can be found at:
122 |
123 |
124 |
125 | Instructions for installing swift packages can be found [here][swift-package-installation].
126 |
127 | ## Versioning
128 |
129 | This project adheres to a [semantic versioning](https://semver.org) paradigm, so breaking changes will be reserved for major version updates.
130 |
131 | ## Version History
132 |
133 | - [1.0.0][tag-1.0.0] Commit initial code
134 |
135 | ## Licensing
136 |
137 | This project is licensed under the MIT License - see [here][mit-licence] for details.
138 |
139 | ## Contact
140 |
141 | You can contact me on Twitter [@CodeSlice][codeslice-twitter]. 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.
142 |
143 |
146 |
147 | [pure-swift-ui-design]: https://github.com/CodeSlicing/pure-swift-ui-design
148 | [codeslice-twitter]: https://twitter.com/CodeSlice
149 | [swift-ui]: https://developer.apple.com/xcode/swiftui/
150 | [swift-functions]: https://docs.swift.org/swift-book/LanguageGuide/Functions.html
151 | [swift-package-installation]: https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app
152 |
153 |
156 |
157 | [gist-pure-swift-ui-design-logo]: https://gist.github.com/CodeSlicing/7865ea405cd23f6ef538ddeefaac8da3
158 |
159 |
162 |
163 | [tag-1.0.0]: https://github.com/CodeSlicing/pure-swift-ui-design/tree/1.0.0
164 |
165 |
166 |
169 |
170 | [docs-layout-guides]: ./Assets/Docs/LayoutGuides/layout-guides.md
171 | [docs-extensions]: ./Assets/Docs/Extensions/extensions.md
172 | [mit-licence]: ./Assets/Docs/LICENCE.md
173 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Model/LayoutGuide/LayoutGuide.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LayoutGuide.swift
3 | //
4 | // Created by Adam Fordyce on 31/01/2020.
5 | // Copyright © 2020 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | public struct LayoutGuide {
9 |
10 | internal var coordinator: LayoutCoordinator
11 | public let rect: CGRect
12 |
13 | internal init(_ coordinator: LayoutCoordinator, rect: CGRect) {
14 | self.coordinator = coordinator
15 | self.rect = rect
16 | }
17 |
18 | var origin: CGPoint {
19 | coordinator.baseOrigin
20 | }
21 |
22 | public subscript(x: Int, y: Int) -> CGPoint {
23 | get {
24 | coordinator[x, y]
25 | }
26 | }
27 |
28 | public subscript(x: Int, y: Int, origin origin: CGPoint) -> CGPoint {
29 | get {
30 | offsetPoint(self[x, y], to: origin)
31 | }
32 | }
33 |
34 | public subscript(rel x: CGFloat, rel y: CGFloat) -> CGPoint {
35 | get {
36 | coordinator[rel: x, rel: y]
37 | }
38 | }
39 |
40 | public subscript(rel x: CGFloat, rel y: CGFloat, origin origin: CGPoint) -> CGPoint {
41 | get {
42 | offsetPoint(self[rel: x, rel: y], to: origin)
43 | }
44 | }
45 |
46 | public subscript(rel x: CGFloat, y: Int) -> CGPoint {
47 | get {
48 | coordinator[rel: x, y]
49 | }
50 | }
51 |
52 | public subscript(rel x: CGFloat, y: Int, origin origin: CGPoint) -> CGPoint {
53 | get {
54 | offsetPoint(self[rel: x, y], to: origin)
55 | }
56 | }
57 |
58 | public subscript(x: Int, rel y: CGFloat) -> CGPoint {
59 | get {
60 | coordinator[x, rel: y]
61 | }
62 | }
63 |
64 | public subscript(x: Int, rel y: CGFloat, origin origin: CGPoint) -> CGPoint {
65 | get {
66 | offsetPoint(self[x, rel: y], to: origin)
67 | }
68 | }
69 |
70 | public var xCount: Int {
71 | coordinator.xCount
72 | }
73 |
74 | public var yCount: Int {
75 | coordinator.yCount
76 | }
77 |
78 | public func radiusTo(_ x: Int, _ y: Int, from: CGPoint) -> CGFloat {
79 | from.radiusTo(self[x, y])
80 | }
81 |
82 | public func radiusTo(_ x: Int, _ y: Int) -> CGFloat {
83 | radiusTo(x, y, from: coordinator.baseOrigin)
84 | }
85 |
86 | public func angleTo(_ x: Int, _ y: Int, from: CGPoint) -> Angle {
87 | from.angleTo(self[x, y])
88 | }
89 |
90 | public func angleTo(_ x: Int, _ y: Int) -> Angle {
91 | angleTo(x, y, from: coordinator.baseOrigin)
92 | }
93 |
94 | internal func anchorLocation(for anchor: UnitPoint) -> CGPoint {
95 | coordinator.anchorLocation(for: anchor, size: rect.size)
96 | }
97 |
98 | private func offsetPoint(_ point: CGPoint, to origin: CGPoint) -> CGPoint {
99 | let delta = origin - coordinator.baseOrigin
100 | return point.offset(delta)
101 | }
102 | }
103 |
104 | // MARK: ----- PROPERTIES
105 |
106 | public extension LayoutGuide {
107 |
108 | var topLeading: CGPoint {
109 | coordinator.topLeading
110 | }
111 |
112 | var top: CGPoint {
113 | coordinator.top
114 | }
115 |
116 | var topTrailing: CGPoint {
117 | coordinator.topTrailing
118 | }
119 |
120 | var trailing: CGPoint {
121 | coordinator.trailing
122 | }
123 |
124 | var bottomTrailing: CGPoint {
125 | coordinator.bottomTrailing
126 | }
127 |
128 | var bottom: CGPoint {
129 | coordinator.bottom
130 | }
131 |
132 | var bottomLeading: CGPoint {
133 | coordinator.bottomLeading
134 | }
135 |
136 | var leading: CGPoint {
137 | coordinator.leading
138 | }
139 |
140 | var center: CGPoint {
141 | coordinator.center
142 | }
143 | }
144 |
145 | // MARK: ----- REFRAMING
146 |
147 | public extension LayoutGuide {
148 |
149 | func reframed(_ rect: CGRect, origin: UnitPoint? = nil) -> LayoutGuide {
150 | LayoutGuide(coordinator.reframed(into: rect, originalRect: self.rect, origin: origin), rect: rect)
151 | }
152 | }
153 |
154 | // MARK: ----- UTILITIES
155 |
156 | internal func calcOrigin(in rect: CGRect, origin: UnitPoint = .topLeading) -> CGPoint {
157 | return rect.origin.moveOrigin(in: rect.size, origin: origin)
158 | }
159 |
160 | // MARK: ----- LAYOUT GUIDE OPERATIONS
161 |
162 | // MARK: ----- ROTATED
163 |
164 | public extension LayoutGuide {
165 |
166 | func rotated(_ angle: Angle, anchor: UnitPoint = .center) -> LayoutGuide {
167 | rotated(angle, anchor: anchor, factor: 1)
168 | }
169 |
170 | func rotated(_ angle: Angle, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuide {
171 | LayoutGuide(RotatedLayoutCoordinator(angle: angle * factor.asDouble, anchor: anchor, anchorPoint: anchorLocation(for: anchor), baseCoordinator: self.coordinator), rect: self.rect)
172 | }
173 | }
174 |
175 | // MARK: ----- ROTATED FROM TO
176 |
177 | public extension LayoutGuide {
178 |
179 | func rotated(from: Angle, to: Angle, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuide {
180 | let delta = to - from
181 | return LayoutGuide(RotatedLayoutCoordinator(angle: from + delta * factor, anchor: anchor, anchorPoint: anchorLocation(for: anchor), baseCoordinator: self.coordinator), rect: self.rect)
182 | }
183 | }
184 |
185 | // MARK: ----- OFFSET
186 |
187 | public extension LayoutGuide {
188 |
189 | func offset(_ offset: CGPoint) -> LayoutGuide {
190 | self.offset(offset, factor: 1)
191 | }
192 |
193 | func offset(_ offset: CGFloat) -> LayoutGuide {
194 | self.offset(.point(offset))
195 | }
196 |
197 | func offset(_ offset: CGPoint, factor: CGFloat) -> LayoutGuide {
198 | LayoutGuide(OffsetLayoutCoordinator(offset: offset.scaled(factor.asCGFloat), baseCoordinator: self.coordinator), rect: self.rect)
199 | }
200 |
201 | func offset(_ offset: CGFloat, factor: CGFloat) -> LayoutGuide {
202 | self.offset(.point(offset), factor: factor)
203 | }
204 | }
205 |
206 | // MARK: ----- OFFSET FROM TO
207 |
208 | public extension LayoutGuide {
209 |
210 | func offset(from: CGPoint, to: CGPoint, factor: CGFloat) -> LayoutGuide {
211 | let delta = to - from
212 | return LayoutGuide(OffsetLayoutCoordinator(offset: from + delta.scaled(factor.asCGFloat), baseCoordinator: self.coordinator), rect: self.rect)
213 | }
214 |
215 | func offset(from: CGSize, to: CGSize, factor: CGFloat) -> LayoutGuide {
216 | offset(from: from.asCGPoint, to: to.asCGPoint, factor: factor)
217 | }
218 | }
219 |
220 | // MARK: ----- OFFSET IN X
221 |
222 | public extension LayoutGuide {
223 |
224 | func xOffset(_ x: CGFloat) -> LayoutGuide {
225 | xOffset(x, factor: 1)
226 | }
227 |
228 | func xOffset(_ x: CGFloat, factor: CGFloat) -> LayoutGuide {
229 | offset(.x(x), factor: factor)
230 | }
231 | }
232 |
233 | // MARK: ----- OFFSET IN X FROM TO
234 |
235 | public extension LayoutGuide {
236 |
237 | func xOffset(from: CGFloat, to: CGFloat, factor: CGFloat) -> LayoutGuide {
238 | let delta = to - from
239 | return offset(.x(from + delta * factor.asCGFloat))
240 | }
241 | }
242 |
243 | // MARK: ----- OFFSET IN Y
244 |
245 | public extension LayoutGuide {
246 |
247 | func yOffset(_ y: CGFloat) -> LayoutGuide {
248 | yOffset(y, factor: 1)
249 | }
250 |
251 | func yOffset(_ y: CGFloat, factor: CGFloat) -> LayoutGuide {
252 | offset(.y(y), factor: factor)
253 | }
254 | }
255 |
256 | // MARK: ----- OFFSET IN Y FROM TO
257 |
258 | public extension LayoutGuide {
259 |
260 | func yOffset(from: CGFloat, to: CGFloat, factor: CGFloat) -> LayoutGuide {
261 | let delta = to - from
262 | return offset(.y(from + delta * factor.asCGFloat))
263 | }
264 | }
265 |
266 | // MARK: ----- SCALED
267 |
268 | public extension LayoutGuide {
269 |
270 | func scaled(_ scale: CGSize, anchor: UnitPoint = .center) -> LayoutGuide {
271 | scaled(scale, anchor: anchor, factor: 1)
272 | }
273 |
274 | func scaled(_ scale: CGFloat, anchor: UnitPoint = .center) -> LayoutGuide {
275 | scaled(.square(scale), anchor: anchor)
276 | }
277 |
278 | func scaled(_ scale: CGSize, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuide {
279 | let effectiveScale = scale - .square(1)
280 | return LayoutGuide(ScaledLayoutCoordinator(scale: .square(1) + effectiveScale.scaled(factor.asCGFloat), anchor: anchor, anchorPoint: anchorLocation(for: anchor), baseCoordinator: self.coordinator), rect: self.rect)
281 | }
282 |
283 | func scaled(_ scale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuide {
284 | scaled(.square(scale), anchor: anchor, factor: factor)
285 | }
286 | }
287 |
288 | // MARK: ----- SCALED FROM TO
289 |
290 | public extension LayoutGuide {
291 |
292 | func scaled(from fromScale: CGSize, to toScale: CGSize, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuide {
293 | let deltaScale = toScale - fromScale
294 | return scaled(fromScale + deltaScale.scaled(factor.asCGFloat), anchor: anchor)
295 | }
296 |
297 | func scaled(from fromScale: CGFloat, to toScale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuide {
298 | scaled(from: .square(fromScale), to: .square(toScale), anchor: anchor, factor: factor)
299 | }
300 | }
301 |
302 | // MARK: ----- XSCALED
303 |
304 | public extension LayoutGuide {
305 |
306 | func xScaled(_ scale: CGFloat, anchor: UnitPoint = .center) -> LayoutGuide {
307 | xScaled(scale, anchor: anchor, factor: 1)
308 | }
309 |
310 | func xScaled(_ scale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuide {
311 | scaled(.size(scale, 1), anchor: anchor, factor: factor)
312 | }
313 | }
314 |
315 | // MARK: ----- XSCALED FROM TO
316 |
317 | public extension LayoutGuide {
318 |
319 | func xScaled(from fromScale: CGFloat, to toScale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuide {
320 | xScaled(fromScale.to(toScale, factor), anchor: anchor)
321 | }
322 | }
323 |
324 | // MARK: ----- YSCALED
325 |
326 | public extension LayoutGuide {
327 |
328 | func yScaled(_ scale: CGFloat, anchor: UnitPoint = .center) -> LayoutGuide {
329 | yScaled(scale, anchor: anchor, factor: 1)
330 | }
331 |
332 | func yScaled(_ scale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuide {
333 | scaled(.size(1, scale), anchor: anchor, factor: factor)
334 | }
335 | }
336 |
337 | // MARK: ----- YSCALED FROM TO
338 |
339 | public extension LayoutGuide {
340 |
341 | func yScaled(from fromScale: CGFloat, to toScale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuide {
342 | return yScaled(fromScale.to(toScale, factor), anchor: anchor)
343 | }
344 | }
345 |
--------------------------------------------------------------------------------
/Tests/PureSwiftUIDesignTests/Extensions/Convenience/CoreGraphics/CGRect+ConvenienceTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGRect+Tests.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 25/11/2019.
6 | //
7 |
8 | import XCTest
9 | @testable import PureSwiftUIDesign
10 |
11 | class CGRectConvenienceExtensionsTests: XCTestCase {
12 |
13 | let x: CGFloat = 2
14 | let y: CGFloat = 4
15 | let width: CGFloat = 8
16 | let height: CGFloat = 10
17 |
18 | var midX: CGFloat {
19 | x + width * 0.5
20 | }
21 |
22 | var midY: CGFloat {
23 | y + height * 0.5
24 | }
25 |
26 | var maxX: CGFloat {
27 | x + width
28 | }
29 |
30 | var maxY: CGFloat {
31 | y + height
32 | }
33 |
34 | var halfWidth: CGFloat {
35 | width * 0.5
36 | }
37 |
38 | var halfHeight: CGFloat {
39 | height * 0.5
40 | }
41 |
42 | var size: CGSize {
43 | CGSize(width: width, height: height)
44 | }
45 |
46 | var rect: CGRect {
47 | CGRect(origin: CGPoint(x: x, y: y), size: CGSize(width: width, height: height))
48 | }
49 | }
50 |
51 | // MARK: ----- INIT
52 |
53 | extension CGRectConvenienceExtensionsTests {
54 |
55 | func testInit() {
56 | XCTAssertEqual(CGRect(x, y, width, height), rect)
57 | XCTAssertEqual(CGRect(width, height), CGRect(0, 0, width, height))
58 | XCTAssertEqual(CGRect(.point(x, y), .size(width, height)), rect)
59 | XCTAssertEqual(CGRect(.size(width, height)), CGRect(0, 0, width, height))
60 | }
61 |
62 | func testInitFromAndToPoint() {
63 | XCTAssertEqual(CGRect(from: .point(x, y), to: .point(x + width, y + height)), rect)
64 | }
65 |
66 | }
67 |
68 | // MARK: ----- INIT WITH ANCHOR
69 |
70 | extension CGRectConvenienceExtensionsTests {
71 |
72 | func testRegionWithSize() {
73 | let expected = CGRect(rect.bottomTrailing.offset(CGPoint(-5, -5)), .square(5))
74 |
75 | XCTAssertEqual(CGRect(rect.bottomTrailing.x, rect.bottomTrailing.y, 5, 5, anchor: .bottomTrailing), expected)
76 | XCTAssertEqual(CGRect(5, 5, anchor: .bottomTrailing), CGRect(-5, -5, 5, 5))
77 | XCTAssertEqual(CGRect(rect.bottomTrailing, .square(5), anchor: .bottomTrailing), expected)
78 |
79 | XCTAssertEqual(CGRect(.size(5, 10), anchor: .bottomTrailing), CGRect(-5, -10, 5, 10))
80 | }
81 | }
82 |
83 | // MARK: ----- STATIC INITIALISERS
84 |
85 | extension CGRectConvenienceExtensionsTests {
86 |
87 | func testStaticInit() {
88 |
89 | XCTAssertEqual(CGRect.rect(from: .point(x, y), to: .point(x + width, y + height)), rect)
90 | XCTAssertEqual(CGRect.rect(.point(x, y), .size(width, height)), rect)
91 | XCTAssertEqual(CGRect.rect(.size(width, height)), CGRect(0, 0, width, height))
92 | XCTAssertEqual(CGRect.rect(x, y, width, height), rect)
93 | }
94 | }
95 |
96 | // MARK: ----- DIMENSIONS
97 |
98 | extension CGRectConvenienceExtensionsTests {
99 |
100 |
101 | func testDimensions() {
102 | XCTAssertEqual(rect.center, CGPoint(midX, midY))
103 | XCTAssertEqual(rect.topLeading, CGPoint(x, y))
104 | XCTAssertEqual(rect.top, CGPoint(midX, y))
105 | XCTAssertEqual(rect.topTrailing, CGPoint(maxX, y))
106 | XCTAssertEqual(rect.leading, CGPoint(x, midY))
107 | XCTAssertEqual(rect.trailing, CGPoint(maxX, midY))
108 | XCTAssertEqual(rect.bottomLeading, CGPoint(x, maxY))
109 | XCTAssertEqual(rect.bottom, CGPoint(midX, maxY))
110 | XCTAssertEqual(rect.bottomTrailing, CGPoint(maxX, maxY))
111 | XCTAssertEqual(rect.extent, rect.bottomTrailing)
112 | XCTAssertEqual(rect.halfWidth, width * 0.5)
113 | XCTAssertEqual(rect.halfHeight, height * 0.5)
114 | XCTAssertEqual(rect.minDimension, width)
115 | XCTAssertEqual(rect.maxDimension, height)
116 | }
117 | }
118 |
119 | // MARK: ----- CLAMPED
120 |
121 | extension CGRectConvenienceExtensionsTests {
122 |
123 | func testClampedSize() {
124 | let expectedResult = CGSize(maxX, maxY)
125 | let result = CGRect(x, y, 0, 20).clampedSize(min: maxX, max: maxY)
126 |
127 | XCTAssertEqual(result, expectedResult)
128 | }
129 | }
130 |
131 | // MARK: ----- SCALED
132 |
133 | extension CGRectConvenienceExtensionsTests {
134 |
135 | func testScaled() {
136 | XCTAssertEqual(rect.widthScaled(0.5), width * 0.5)
137 | XCTAssertEqual(rect.heightScaled(0.5), height * 0.5)
138 | XCTAssertEqual(rect.sizeScaled(0.5), CGSize(width * 0.5, height * 0.5))
139 | XCTAssertEqual(rect.sizeScaled(.point(0.5)), CGSize(width * 0.5, height * 0.5))
140 | XCTAssertEqual(rect.sizeScaled(.vector(0.5)), CGSize(width * 0.5, height * 0.5))
141 | XCTAssertEqual(rect.sizeScaled(.size(0.5)), CGSize(width * 0.5, height * 0.5))
142 | XCTAssertEqual(rect.sizeScaled(0.1, 0.5), CGSize(width * 0.1, height * 0.5))
143 | }
144 | }
145 |
146 | // MARK: ----- RELATIVE
147 |
148 | extension CGRectConvenienceExtensionsTests {
149 |
150 | func testRelative() {
151 | XCTAssertEqual(rect.relativeX(0.5), rect.midX)
152 | XCTAssertEqual(rect.relativeY(0.5), rect.midY)
153 | XCTAssertEqual(rect.relativeX(0), rect.minX)
154 | XCTAssertEqual(rect.relativeY(0), rect.minY)
155 | XCTAssertEqual(rect.relativeX(1), rect.maxX)
156 | XCTAssertEqual(rect.relativeY(1), rect.maxY)
157 | XCTAssertEqual(rect.relativeX(0.25), rect.minX + rect.widthScaled(0.25))
158 | XCTAssertEqual(rect.relativeY(0.75), rect.minY + rect.heightScaled(0.75))
159 | }
160 |
161 | func testRelativeSubscript() {
162 | XCTAssertEqual(rect[0, 0], rect.origin)
163 | XCTAssertEqual(rect[1, 1], rect.extent)
164 | XCTAssertEqual(rect[1, 1], rect.bottomTrailing)
165 | XCTAssertEqual(rect[0.5, 0.5], rect.center)
166 | XCTAssertEqual(rect[0, 0.5], rect.leading)
167 | XCTAssertEqual(rect[0.5, 1], rect.bottom)
168 | XCTAssertEqual(rect[0.25, 0.75], CGPoint(rect.relativeX(0.25), rect.relativeY(0.75)))
169 | }
170 | }
171 |
172 | // MARK: ----- INSET
173 |
174 | #if !os(macOS)
175 | extension CGRectConvenienceExtensionsTests {
176 |
177 | func testInset() {
178 | let topInset: CGFloat = 1
179 | let leadingInset: CGFloat = 2
180 | let bottomInset: CGFloat = 3
181 | let trailingInset: CGFloat = 4
182 |
183 | XCTAssertEqual(rect.inset(topInset, leadingInset, bottomInset, trailingInset),
184 | CGRect(x + leadingInset, y + topInset, width - trailingInset - leadingInset, height - bottomInset - topInset))
185 | XCTAssertEqual(rect.inset([.top, .trailing], leadingInset),
186 | CGRect(x, y + leadingInset, width - leadingInset, height - leadingInset))
187 |
188 | XCTAssertEqual(rect.insetTop(topInset), rect.inset(.top, topInset))
189 |
190 | XCTAssertEqual(rect.insetLeading(leadingInset), rect.inset(.leading, leadingInset))
191 |
192 | XCTAssertEqual(rect.insetBottom(bottomInset), rect.inset(.bottom, bottomInset))
193 |
194 | XCTAssertEqual(rect.insetTrailing(trailingInset), rect.inset(.trailing, trailingInset))
195 |
196 | XCTAssertEqual(rect.hInset(leadingInset), rect.inset([.leading, .trailing], leadingInset))
197 | XCTAssertEqual(rect.vInset(topInset), rect.inset([.top, .bottom], topInset))
198 |
199 | XCTAssertEqual(rect.inset(topInset), rect.inset(.all, topInset))
200 | }
201 | }
202 | #endif
203 |
204 | // MARK: ----- SCALE
205 |
206 | extension CGRectConvenienceExtensionsTests {
207 |
208 | func testRegionWithScale() {
209 | XCTAssertEqual(rect.scaled(CGSize(0.5, 0.5), at: rect.bottomTrailing, anchor: .bottomTrailing),
210 | CGRect(rect.bottomTrailing.offset(rect.sizeScaled(-0.5)), CGSize(4, 5)))
211 | XCTAssertEqual(rect.scaled(0.5, at: rect.bottomTrailing, anchor: .bottomTrailing),
212 | CGRect(rect.bottomTrailing.offset(rect.sizeScaled(-0.5)), CGSize(4, 5)))
213 | XCTAssertEqual(rect.xScaled(0.5, at: rect.bottomTrailing, anchor: .bottomTrailing),
214 | CGRect(rect.top, CGSize(4, 10)))
215 | XCTAssertEqual(rect.yScaled(0.5, at: rect.bottomTrailing, anchor: .bottomTrailing),
216 | CGRect(rect.leading, CGSize(8, 5)))
217 | }
218 | }
219 |
220 | // MARK: ----- OFFSET
221 |
222 | extension CGRectConvenienceExtensionsTests {
223 |
224 | func testOffsetAnchor() {
225 | XCTAssertEqual(rect.offset(anchor: .bottomTrailing), CGRect(x - width, y - height, width, height))
226 | XCTAssertEqual(rect.offset(anchor: UnitPoint(-0.5, -0.5)), CGRect(x + 4, y + 5, width, height))
227 |
228 | let expected = CGRect(CGPoint(2 * x, 2 * y), size)
229 | XCTAssertEqual(rect.offset(x, y), expected)
230 | XCTAssertEqual(rect.offset(CGPoint(x, y)), expected)
231 | XCTAssertEqual(rect.offset(CGSize(x, y)), expected)
232 | XCTAssertEqual(rect.offset(CGVector(x, y)), expected)
233 |
234 | XCTAssertEqual(rect.xOffset(x), CGRect(CGPoint(x + x, y), size))
235 | XCTAssertEqual(rect.yOffset(y), CGRect(CGPoint(x, y + y), size))
236 | }
237 | }
238 |
239 | // MARK: ----- OFFSET WITH FACTOR
240 |
241 | extension CGRectConvenienceExtensionsTests {
242 |
243 | func testOffsetAnchorWithFactor() {
244 | XCTAssertEqual(rect.offset(anchor: .bottomTrailing, factor: 0.5), CGRect(x - width * 0.5, y - height * 0.5, width, height))
245 | XCTAssertEqual(rect.offset(anchor: .bottomTrailing, factor: .square(0.5)), CGRect(x - width * 0.5, y - height * 0.5, width, height))
246 | XCTAssertEqual(rect.offset(anchor: UnitPoint(-0.5, -0.5), factor: 0.5), CGRect(x + 2, y + 2.5, width, height))
247 |
248 | let expected = CGRect(CGPoint(x + x * 0.5, y + y * 0.5), size)
249 | XCTAssertEqual(rect.offset(x, y, factor: 0.5), expected)
250 | XCTAssertEqual(rect.offset(x, y, factor: .square(0.5)), expected)
251 | XCTAssertEqual(rect.offset(CGPoint(x, y), factor: 0.5), expected)
252 | XCTAssertEqual(rect.offset(CGSize(x, y), factor: 0.5), expected)
253 | XCTAssertEqual(rect.offset(CGVector(x, y), factor: 0.5), expected)
254 | XCTAssertEqual(rect.offset(CGPoint(x, y), factor: .square(0.5)), expected)
255 | XCTAssertEqual(rect.offset(CGSize(x, y), factor: .square(0.5)), expected)
256 | XCTAssertEqual(rect.offset(CGVector(x, y), factor: .square(0.5)), expected)
257 |
258 | XCTAssertEqual(rect.xOffset(x, factor: 0.5), CGRect(CGPoint(x + x * 0.5, y), size))
259 | XCTAssertEqual(rect.yOffset(y, factor: 0.5), CGRect(CGPoint(x, y + y * 0.5), size))
260 | }
261 | }
262 |
263 | // MARK: ----- TO another rect with a factor
264 |
265 | extension CGRectConvenienceExtensionsTests {
266 |
267 | func testToAnotherRectWithFactor() {
268 | let endWidth = width * 2
269 | let endHeight = height * 4
270 | let startingRect = CGRect(0, 0, width, height)
271 | let endingRect = CGRect(x, y, endWidth, endHeight)
272 |
273 | XCTAssertEqual(startingRect.to(endingRect, 0), startingRect)
274 | XCTAssertEqual(startingRect.to(endingRect, 1), endingRect)
275 | XCTAssertEqual(startingRect.to(endingRect, 0.5), CGRect(x * 0.5, y * 0.5, width * 1.5, height * 2.5))
276 |
277 | XCTAssertEqual(startingRect.to(endingRect, .size(0, 0)), startingRect)
278 | XCTAssertEqual(startingRect.to(endingRect, .size(1, 1)), endingRect)
279 | XCTAssertEqual(startingRect.to(endingRect, .size(0, 1)), CGRect(0, y, width, endHeight))
280 | XCTAssertEqual(startingRect.to(endingRect, .size(1, 0)), CGRect(x, 0, endWidth, height))
281 | XCTAssertEqual(startingRect.to(endingRect, .size(0.5, 0.5)), CGRect(x * 0.5, y * 0.5, width * 1.5, height * 2.5))
282 | }
283 | }
284 |
285 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/CoreGraphics/CGRect+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGRect+Convenience.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 11/11/2019.
6 | // Copyright © 2019 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | import CoreGraphics
10 |
11 | // MARK: ----- INIT
12 |
13 | public extension CGRect {
14 |
15 | init(_ x: CGFloat, _ y: CGFloat, _ width: CGFloat, _ height: CGFloat) {
16 | self.init(origin: CGPoint(x, y), size: CGSize(width, height))
17 | }
18 |
19 | init(_ size: CGSize) {
20 | self.init(size.width, size.height)
21 | }
22 |
23 | init(_ width: CGFloat, _ height: CGFloat) {
24 | self.init(0, 0, width, height)
25 | }
26 |
27 | init(_ origin: CGPoint, _ size: CGSize) {
28 | self.init(origin.x, origin.y, size.width, size.height)
29 | }
30 | }
31 |
32 | // MARK: ----- INIT WITH ANCHOR
33 |
34 | public extension CGRect {
35 |
36 | init(_ x: CGFloat, _ y: CGFloat, _ width: CGFloat, _ height: CGFloat, anchor: UnitPoint) {
37 | let size = CGSize(width, height)
38 | self.init(origin: CGPoint(x, y).offset(in: size, anchor: anchor), size: CGSize(width, height))
39 | }
40 |
41 | init(_ size: CGSize, anchor: UnitPoint) {
42 | self.init(size.width, size.height, anchor: anchor)
43 | }
44 |
45 | init(_ width: CGFloat, _ height: CGFloat, anchor: UnitPoint) {
46 | self.init(0, 0, width, height, anchor: anchor)
47 | }
48 |
49 | init(_ origin: CGPoint, _ size: CGSize, anchor: UnitPoint) {
50 | self.init(origin.x, origin.y, size.width, size.height, anchor: anchor)
51 | }
52 | }
53 |
54 | // MARK: ----- INIT FROM POINT TO POINT
55 |
56 | public extension CGRect {
57 |
58 | init(from: CGPoint, to: CGPoint) {
59 | let origin = CGPoint(min(from.x, to.x), min(from.y, to.y))
60 | let size = CGSize(abs(from.x - to.x), abs(from.y - to.y))
61 | self.init(origin, size)
62 | }
63 | }
64 |
65 | // MARK: ----- STATIC INITIALISER
66 |
67 | public extension CGRect {
68 |
69 | static func rect(from: CGPoint, to: CGPoint) -> CGRect {
70 | .init(from: from, to: to)
71 | }
72 |
73 | static func rect(_ size: CGSize) -> CGRect {
74 | .init(.zero, size)
75 | }
76 |
77 | static func rect(_ origin: CGPoint, _ size: CGSize) -> CGRect {
78 | .init(origin, size)
79 | }
80 |
81 | static func rect(_ x: CGFloat, _ y: CGFloat, _ width: CGFloat, _ height: CGFloat) -> CGRect {
82 | .init(x, y, width, height)
83 | }
84 | }
85 |
86 | // MARK: ----- PROPERTIES
87 |
88 | public extension CGRect {
89 |
90 | var center: CGPoint {
91 | .init(halfWidth + origin.x, halfHeight + origin.y)
92 | }
93 |
94 | var topLeading: CGPoint {
95 | origin
96 | }
97 |
98 | var top: CGPoint {
99 | CGPoint(midX, minY)
100 | }
101 |
102 | var topTrailing: CGPoint {
103 | CGPoint(maxX, minY)
104 | }
105 |
106 | var leading: CGPoint {
107 | CGPoint(minX, midY)
108 | }
109 |
110 | var trailing: CGPoint {
111 | CGPoint(maxX, midY)
112 | }
113 |
114 | var bottomLeading: CGPoint {
115 | CGPoint(minX, maxY)
116 | }
117 |
118 | var bottom: CGPoint {
119 | CGPoint(midX, maxY)
120 | }
121 |
122 | var bottomTrailing: CGPoint {
123 | CGPoint(maxX, maxY)
124 | }
125 |
126 | /**
127 | alias for bottomTrailing
128 | */
129 | var extent: CGPoint {
130 | bottomTrailing
131 | }
132 |
133 | var halfWidth: CGFloat {
134 | size.halfWidth
135 | }
136 |
137 | var halfHeight: CGFloat {
138 | size.halfHeight
139 | }
140 |
141 | var minDimension: CGFloat {
142 | size.minDimension
143 | }
144 |
145 | var maxDimension: CGFloat {
146 | size.maxDimension
147 | }
148 |
149 | func clampedSize(min: CGFloat, max: CGFloat) -> CGSize {
150 | .init(self.width.clamped(min: min, max: max), self.height.clamped(min: min, max: max))
151 | }
152 |
153 | func widthScaled(_ scale: CGFloat) -> CGFloat {
154 | width * scale
155 | }
156 |
157 | func heightScaled(_ scale: CGFloat) -> CGFloat {
158 | height * scale
159 | }
160 | }
161 |
162 | // MARK: ----- SCALED
163 |
164 | public extension CGRect {
165 |
166 | func sizeScaled(_ scale: CGPoint) -> CGSize {
167 | CGSize(widthScaled(scale.x), heightScaled(scale.y))
168 | }
169 |
170 | func sizeScaled(_ scale: CGVector) -> CGSize {
171 | CGSize(widthScaled(scale.dx), heightScaled(scale.dy))
172 | }
173 |
174 | func sizeScaled(_ scale: CGSize) -> CGSize {
175 | CGSize(widthScaled(scale.width), heightScaled(scale.height))
176 | }
177 |
178 | func sizeScaled(_ scale: CGFloat) -> CGSize {
179 | CGSize(widthScaled(scale), heightScaled(scale))
180 | }
181 |
182 | func sizeScaled(_ xScale: CGFloat, _ yScale: CGFloat) -> CGSize {
183 | CGSize(widthScaled(xScale), heightScaled(yScale))
184 | }
185 | }
186 |
187 | // MARK: ----- RELATIVE
188 |
189 | public extension CGRect {
190 |
191 | /**
192 | Creates a `CGFloat` relative to the x-coordinate space of the `CGRect` on which
193 | it is invoked.
194 |
195 | To be specific, for a `relativeX` parameter value of 0.5, the resulting value
196 | will be `rect.minX + 0.5 * rect.width`
197 |
198 | - Parameter relativeX: The relative x-coordinate of the required value
199 | */
200 | func relativeX(_ relativeX: CGFloat) -> CGFloat {
201 | minX + widthScaled(relativeX)
202 | }
203 |
204 | /**
205 | Creates a `CGFloat` relative to the y-coordinate space of the `CGRect` on which
206 | it is invoked.
207 |
208 | To be specific, for a `relativeY` parameter value of 0.5, the resulting value
209 | will be `rect.minY + 0.5 * rect.height`
210 |
211 | - Parameter relativeY: The relative y-coordinate of the required value
212 | */
213 | func relativeY(_ relativeY: CGFloat) -> CGFloat {
214 | minY + heightScaled(relativeY)
215 | }
216 |
217 | /**
218 | Obtains a `CGPoint` from a `CGRect` relative to the `CGRect` itself taking
219 | into account the origin.
220 |
221 | To be specific, for a `relativeX` paremeter value of 0.5, the resulting x-coordinate
222 | of the `CGPoint` will be `rect.minX + 0.5 * rect.width`.
223 |
224 | - Parameter relativeX: The relative position of the x-coordinate
225 | - Parameter relativeY: The relative position of the y-coordinate
226 | */
227 | subscript(relativeX: CGFloat, relativeY: CGFloat) -> CGPoint {
228 | CGPoint(self.relativeX(relativeX), self.relativeY(relativeY))
229 | }
230 | }
231 |
232 | // MARK: ----- INSET
233 |
234 | #if !os(macOS)
235 | public extension CGRect {
236 |
237 | func inset(_ top: CGFloat, _ leading: CGFloat, _ bottom: CGFloat, _ trailing: CGFloat) -> CGRect {
238 | inset(by: UIEdgeInsets(top: top, left: leading, bottom: bottom, right: trailing))
239 | }
240 |
241 | func inset(_ edges: Edge.Set, _ size: CGFloat) -> CGRect {
242 | inset(edges.inset(.top, size), edges.inset(.leading, size), edges.inset(.bottom, size), edges.inset(.trailing, size))
243 | }
244 |
245 | func insetTop(_ size: CGFloat) -> CGRect {
246 | inset(size, 0, 0, 0)
247 | }
248 |
249 | func insetLeading(_ size: CGFloat) -> CGRect {
250 | inset(0, size, 0, 0)
251 | }
252 |
253 | func insetBottom(_ size: CGFloat) -> CGRect {
254 | inset(0, 0, size, 0)
255 | }
256 |
257 | func insetTrailing(_ size: CGFloat) -> CGRect {
258 | inset(0, 0, 0, size)
259 | }
260 |
261 | func hInset(_ size: CGFloat) -> CGRect {
262 | inset(0, size, 0, size)
263 | }
264 |
265 | func vInset(_ size: CGFloat) -> CGRect {
266 | inset(size, 0, size, 0)
267 | }
268 |
269 | func inset(_ size: CGFloat) -> CGRect {
270 | inset(size, size, size, size)
271 | }
272 | }
273 | #endif
274 |
275 | // MARK: ----- REGION
276 |
277 | public extension CGRect {
278 |
279 | func scaled(_ scale: CGSize, at location: CGPoint, anchor: UnitPoint = .topLeading) -> CGRect {
280 | CGRect(location, sizeScaled(scale), anchor: anchor)
281 | }
282 |
283 | func scaled(_ scale: CGFloat, at location: CGPoint, anchor: UnitPoint = .topLeading) -> CGRect {
284 | scaled(.square(scale), at: location, anchor: anchor)
285 | }
286 |
287 | func xScaled(_ scale: CGFloat, at location: CGPoint, anchor: UnitPoint = .topLeading) -> CGRect {
288 | scaled(CGSize(scale, 1), at: location, anchor: anchor)
289 | }
290 |
291 | func yScaled(_ scale:CGFloat, at location: CGPoint, anchor: UnitPoint = .topLeading) -> CGRect {
292 | scaled(CGSize(1, scale), at: location, anchor: anchor)
293 | }
294 | }
295 |
296 | // MARK: ----- OFFSET
297 |
298 | public extension CGRect {
299 |
300 | func offset(anchor: UnitPoint) -> CGRect {
301 | CGRect(self.origin.offset(in: size, anchor: anchor), size)
302 | }
303 |
304 | func offset(_ x: CGFloat, _ y: CGFloat) -> CGRect {
305 | offsetBy(dx: x, dy: y)
306 | }
307 |
308 | func offset(_ point: CGPoint) -> CGRect {
309 | offset(point.x, point.y)
310 | }
311 |
312 | func offset(_ point: CGSize) -> CGRect {
313 | offset(point.width, point.height)
314 | }
315 |
316 | func offset(_ point: CGVector) -> CGRect {
317 | offset(point.dx, point.dy)
318 | }
319 |
320 | func xOffset(_ x: CGFloat) -> CGRect {
321 | offset(x, 0)
322 | }
323 |
324 | func yOffset(_ y: CGFloat) -> CGRect {
325 | offset(0, y)
326 | }
327 | }
328 |
329 | // MARK: ----- OFFSET SCALED
330 |
331 | public extension CGRect {
332 |
333 | func offset(anchor: UnitPoint, factor: CGFloat) -> CGRect {
334 | CGRect(self.origin.offset(in: size, anchor: anchor, factor: factor), size)
335 | }
336 |
337 | func offset(anchor: UnitPoint, factor: CGSize) -> CGRect {
338 | CGRect(self.origin.offset(in: size, anchor: anchor, factor: factor), size)
339 | }
340 |
341 | func offset(_ x: CGFloat, _ y: CGFloat, factor: CGFloat) -> CGRect {
342 | offset(x * factor, y * factor)
343 | }
344 |
345 | func offset(_ x: CGFloat, _ y: CGFloat, factor: CGSize) -> CGRect {
346 | offset(x * factor.x, y * factor.y)
347 | }
348 |
349 | func offset(_ point: CGPoint, factor: CGFloat) -> CGRect {
350 | offset(point.x, point.y, factor: factor)
351 | }
352 |
353 | func offset(_ point: CGPoint, factor: CGSize) -> CGRect {
354 | offset(point.x, point.y, factor: factor)
355 | }
356 |
357 | func offset(_ point: CGSize, factor: CGFloat) -> CGRect {
358 | offset(point.width, point.height, factor: factor)
359 | }
360 |
361 | func offset(_ point: CGSize, factor: CGSize) -> CGRect {
362 | offset(point.width, point.height, factor: factor)
363 | }
364 |
365 | func offset(_ point: CGVector, factor: CGFloat) -> CGRect {
366 | offset(point.dx, point.dy, factor: factor)
367 | }
368 |
369 | func offset(_ point: CGVector, factor: CGSize) -> CGRect {
370 | offset(point.dx, point.dy, factor: factor)
371 | }
372 |
373 | func xOffset(_ x: CGFloat, factor: CGFloat) -> CGRect {
374 | offset(x, 0, factor: factor)
375 | }
376 |
377 | func yOffset(_ y: CGFloat, factor: CGFloat) -> CGRect {
378 | offset(0, y, factor: factor)
379 | }
380 | }
381 |
382 | // MARK: ----- TO WITH FACTOR
383 |
384 | public extension CGRect {
385 |
386 | func to(_ destination: CGRect, _ factor: CGFloat) -> CGRect {
387 | to(destination, .square(factor))
388 | }
389 |
390 | func to(_ destination: CGRect, _ factor: CGSize) -> CGRect {
391 | CGRect(
392 | from: origin.to(destination.origin, factor),
393 | to: bottomTrailing.to(destination.bottomTrailing, factor)
394 | )
395 | }
396 | }
397 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Model/LayoutGuide/LayoutGuideConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LayoutGuideConfig.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 04/02/2020.
6 | // Copyright © 2020 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct LayoutGuideConfig: Shape {
12 |
13 | fileprivate let layoutProvider: (CGRect) -> LayoutGuide
14 | fileprivate let layoutPathProvider: (LayoutGuide, CGRect) -> Path
15 |
16 | public func path(in rect: CGRect) -> Path {
17 | let layout = layoutProvider(rect)
18 | return layoutPathProvider(layout, rect)
19 | }
20 |
21 | public func layout(in rect: CGRect) -> LayoutGuide {
22 | layoutProvider(rect)
23 | }
24 | }
25 |
26 | // MARK: ----- ROTATED
27 |
28 | public extension LayoutGuideConfig {
29 |
30 | func rotated(_ angle: Angle, anchor: UnitPoint = .center) -> LayoutGuideConfig {
31 | rotated(angle, anchor: anchor, factor: 1)
32 | }
33 |
34 | func rotated(_ angle: Angle, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuideConfig {
35 | LayoutGuideConfig(layoutProvider: { rect in
36 | self.layoutProvider(rect).rotated(angle, anchor: anchor, factor: factor)
37 | }, layoutPathProvider: self.layoutPathProvider)
38 | }
39 | }
40 |
41 | // MARK: ----- ROTATED FROM TO
42 |
43 | public extension LayoutGuideConfig {
44 |
45 | func rotated(from: Angle, to: Angle, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuideConfig {
46 | LayoutGuideConfig(layoutProvider: { rect in
47 | self.layoutProvider(rect).rotated(from: from, to: to, anchor: anchor, factor: factor)
48 | }, layoutPathProvider: self.layoutPathProvider)
49 | }
50 | }
51 |
52 | // MARK: ----- OFFSET
53 |
54 | public extension LayoutGuideConfig {
55 |
56 | func offset(_ offset: CGPoint) -> LayoutGuideConfig {
57 | self.offset(offset, factor: 1)
58 | }
59 |
60 | func offset(_ offset: CGFloat) -> LayoutGuideConfig {
61 | self.offset(.point(offset))
62 | }
63 |
64 | func offset(_ offset: CGPoint, factor: CGFloat) -> LayoutGuideConfig {
65 | LayoutGuideConfig(layoutProvider: { rect in
66 | self.layoutProvider(rect).offset(offset, factor: factor)
67 | }, layoutPathProvider: self.layoutPathProvider)
68 | }
69 |
70 | func offset(_ offset: CGFloat, factor: CGFloat) -> LayoutGuideConfig {
71 | self.offset(.point(offset), factor: factor)
72 | }
73 | }
74 |
75 | // MARK: ----- OFFSET FROM TO
76 |
77 | public extension LayoutGuideConfig {
78 |
79 | func offset(from: CGPoint, to: CGPoint, factor: CGFloat) -> LayoutGuideConfig {
80 | LayoutGuideConfig(layoutProvider: { rect in
81 | self.layoutProvider(rect).offset(from: from, to: to, factor: factor)
82 | }, layoutPathProvider: self.layoutPathProvider)
83 | }
84 |
85 | func offset(from: CGSize, to: CGSize, factor: CGFloat) -> LayoutGuideConfig {
86 | offset(from: from.asCGPoint, to: to.asCGPoint, factor: factor)
87 | }
88 | }
89 |
90 | // MARK: ----- OFFSET IN X
91 |
92 | public extension LayoutGuideConfig {
93 |
94 | func xOffset(_ x: CGFloat) -> LayoutGuideConfig {
95 | xOffset(x, factor: 1)
96 | }
97 |
98 | func xOffset(_ x: CGFloat, factor: CGFloat) -> LayoutGuideConfig {
99 | offset(.x(x), factor: factor)
100 | }
101 | }
102 |
103 | // MARK: ----- OFFSET IN X FROM TO
104 |
105 | public extension LayoutGuideConfig {
106 |
107 | func xOffset(from: CGFloat, to: CGFloat, factor: CGFloat) -> LayoutGuideConfig {
108 | let delta = to - from
109 | return offset(.x(from + delta * factor.asCGFloat))
110 | }
111 | }
112 |
113 | // MARK: ----- OFFSET IN Y
114 |
115 | public extension LayoutGuideConfig {
116 |
117 | func yOffset(_ y: CGFloat) -> LayoutGuideConfig {
118 | yOffset(y, factor: 1)
119 | }
120 |
121 | func yOffset(_ y: CGFloat, factor: CGFloat) -> LayoutGuideConfig {
122 | offset(.y(y), factor: factor)
123 | }
124 | }
125 |
126 | // MARK: ----- OFFSET IN Y FROM TO
127 |
128 | public extension LayoutGuideConfig {
129 |
130 | func yOffset(from: CGFloat, to: CGFloat, factor: CGFloat) -> LayoutGuideConfig {
131 | let delta = to - from
132 | return offset(.y(from + delta * factor.asCGFloat))
133 | }
134 | }
135 |
136 | // MARK: ----- SCALED
137 |
138 | public extension LayoutGuideConfig {
139 |
140 | func scaled(_ scale: CGSize, anchor: UnitPoint = .center) -> LayoutGuideConfig {
141 | scaled(scale, anchor: anchor, factor: 1)
142 | }
143 |
144 | func scaled(_ scale: CGFloat, anchor: UnitPoint = .center) -> LayoutGuideConfig {
145 | scaled(.square(scale), anchor: anchor)
146 | }
147 |
148 | func scaled(_ scale: CGSize, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuideConfig {
149 | LayoutGuideConfig(layoutProvider: { rect in
150 | self.layoutProvider(rect).scaled(scale, anchor: anchor, factor: factor)
151 | }, layoutPathProvider: self.layoutPathProvider)
152 | }
153 |
154 | func scaled(_ scale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuideConfig {
155 | scaled(.square(scale), anchor: anchor, factor: factor)
156 | }
157 | }
158 |
159 | // MARK: ----- SCALED FROM TO
160 |
161 | public extension LayoutGuideConfig {
162 |
163 | func scaled(from fromScale: CGSize, to toScale: CGSize, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuideConfig {
164 | LayoutGuideConfig(layoutProvider: { rect in
165 | self.layoutProvider(rect).scaled(from: fromScale, to: toScale, anchor: anchor, factor: factor)
166 | }, layoutPathProvider: self.layoutPathProvider)
167 | }
168 |
169 | func scaled(from fromScale: CGFloat, to toScale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuideConfig {
170 | scaled(from: .square(fromScale), to: .square(toScale), anchor: anchor, factor: factor)
171 | }
172 | }
173 |
174 | // MARK: ----- XSCALED
175 |
176 | public extension LayoutGuideConfig {
177 |
178 | func xScaled(_ scale: CGFloat, anchor: UnitPoint = .center) -> LayoutGuideConfig {
179 | xScaled(scale, anchor: anchor, factor: 1)
180 | }
181 |
182 | func xScaled(_ scale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuideConfig {
183 | scaled(.size(scale, 1), anchor: anchor, factor: factor)
184 | }
185 | }
186 |
187 | // MARK: ----- XSCALED FROM TO
188 |
189 | public extension LayoutGuideConfig {
190 |
191 | func xScaled(from fromScale: CGFloat, to toScale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuideConfig {
192 | scaled(from: .size(fromScale, 1), to: .size(toScale, 1), anchor: anchor, factor: factor)
193 | }
194 | }
195 |
196 | // MARK: ----- YSCALED
197 |
198 | public extension LayoutGuideConfig {
199 |
200 | func yScaled(_ scale: CGFloat, anchor: UnitPoint = .center) -> LayoutGuideConfig {
201 | yScaled(scale, anchor: anchor, factor: 1)
202 | }
203 |
204 | func yScaled(_ scale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuideConfig {
205 | scaled(.size(1, scale), anchor: anchor, factor: factor)
206 | }
207 | }
208 |
209 | // MARK: ----- YSCALED FROM TO
210 |
211 | public extension LayoutGuideConfig {
212 |
213 | func yScaled(from fromScale: CGFloat, to toScale: CGFloat, anchor: UnitPoint = .center, factor: CGFloat) -> LayoutGuideConfig {
214 | scaled(from: .size(1, fromScale), to: .size(1, toScale), anchor: anchor, factor: factor)
215 | }
216 | }
217 |
218 | // MARK: ----- GRID CONFIGURATIONS
219 |
220 | public extension LayoutGuideConfig {
221 |
222 | static func grid(columns: Int, rows: Int, origin: UnitPoint = .topLeading) -> LayoutGuideConfig {
223 | LayoutGuideConfig(layoutProvider: { rect in
224 | LayoutGuide.grid(rect, columns: columns, rows: rows, origin: origin)
225 | }, layoutPathProvider: gridLayoutPathProvider)
226 | }
227 |
228 | static func grid(columns: [CGFloat], rows: Int, origin: UnitPoint = .topLeading) -> LayoutGuideConfig {
229 | LayoutGuideConfig(layoutProvider: { rect in
230 | LayoutGuide.grid(rect, columns: columns, rows: rows, origin: origin)
231 | }, layoutPathProvider: gridLayoutPathProvider)
232 | }
233 |
234 | static func grid(columns: Int, rows: [CGFloat], origin: UnitPoint = .topLeading) -> LayoutGuideConfig {
235 | LayoutGuideConfig(layoutProvider: { rect in
236 | LayoutGuide.grid(rect, columns: columns, rows: rows, origin: origin)
237 | }, layoutPathProvider: gridLayoutPathProvider)
238 | }
239 |
240 | static func grid(columns: [CGFloat], rows: [CGFloat], origin: UnitPoint = .topLeading) -> LayoutGuideConfig {
241 | LayoutGuideConfig(layoutProvider: { rect in
242 | LayoutGuide.grid(rect, columns: columns, rows: rows, origin: origin)
243 | }, layoutPathProvider: gridLayoutPathProvider)
244 | }
245 | }
246 |
247 | // MARK: ----- POLAR CONFIGURATIONS
248 |
249 | public extension LayoutGuideConfig {
250 |
251 | static func polar(rings: Int, segments: Int, useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuideConfig {
252 | LayoutGuideConfig(layoutProvider: { rect in
253 | LayoutGuide.polar(rect, rings: rings, segments: segments, useMaxDimension: useMaxDimension, origin: origin, fromTop: fromTop)
254 | }, layoutPathProvider: polarLayoutPathProvider)
255 | }
256 |
257 | static func polar(rings: [CGFloat], segments: Int, useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuideConfig {
258 | LayoutGuideConfig(layoutProvider: { rect in
259 | LayoutGuide.polar(rect, rings: rings, segments: segments, useMaxDimension: useMaxDimension, origin: origin, fromTop: fromTop)
260 | }, layoutPathProvider: polarLayoutPathProvider)
261 | }
262 |
263 | static func polar(rings: Int, segments: [CGFloat], useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuideConfig {
264 | LayoutGuideConfig(layoutProvider: { rect in
265 | LayoutGuide.polar(rect, rings: rings, segments: segments, useMaxDimension: useMaxDimension, origin: origin, fromTop: fromTop)
266 | }, layoutPathProvider: polarLayoutPathProvider)
267 | }
268 |
269 | static func polar(rings: [CGFloat], segments: [CGFloat], useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuideConfig {
270 | LayoutGuideConfig(layoutProvider: { rect in
271 | LayoutGuide.polar(rect, rings: rings, segments: segments, useMaxDimension: useMaxDimension, origin: origin, fromTop: fromTop)
272 | }, layoutPathProvider: polarLayoutPathProvider)
273 | }
274 |
275 | static func polar(rings: Int, segments: [Angle], useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuideConfig {
276 | LayoutGuideConfig(layoutProvider: { rect in
277 | LayoutGuide.polar(rect, rings: rings, segments: segments, useMaxDimension: useMaxDimension, origin: origin, fromTop: fromTop)
278 | }, layoutPathProvider: polarLayoutPathProvider)
279 | }
280 |
281 | static func polar(rings: [CGFloat], segments: [Angle], useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuideConfig {
282 | LayoutGuideConfig(layoutProvider: { rect in
283 | LayoutGuide.polar(rect, rings: rings, segments: segments, useMaxDimension: useMaxDimension, origin: origin, fromTop: fromTop)
284 | }, layoutPathProvider: polarLayoutPathProvider)
285 | }
286 | }
287 |
288 | private func gridLayoutPathProvider(_ gridLayout: LayoutGuide, rect: CGRect) -> Path {
289 | var path = Path()
290 | let layoutCopy = gridLayout
291 |
292 | for x in 0...gridLayout.xCount {
293 | for y in 0...gridLayout.yCount {
294 |
295 | path.line(from: layoutCopy[x, 0], to: layoutCopy[x, layoutCopy.yCount])
296 | path.line(from: layoutCopy[0, y], to: layoutCopy[layoutCopy.xCount, y])
297 | }
298 | }
299 | return path
300 | }
301 |
302 | private func polarLayoutPathProvider(_ gridLayout: LayoutGuide, rect: CGRect) -> Path {
303 | var path = Path()
304 | let layoutCopy = gridLayout
305 |
306 | for x in 0...gridLayout.xCount {
307 | path.ellipse(layoutCopy.origin, .square(layoutCopy.radiusTo(x, 0) * 2), anchor: .center)
308 | }
309 |
310 | let spokeLength = layoutCopy.radiusTo(gridLayout.xCount, 0)
311 | for y in 0.. CGPoint {
72 | scaled(.square(scale))
73 | }
74 |
75 | func scaled(_ xScale: CGFloat, _ yScale: CGFloat) -> CGPoint {
76 | CGPoint(xScaled(xScale), yScaled(yScale))
77 | }
78 |
79 | func scaled(_ scale: CGSize) -> CGPoint {
80 | scaled(scale.width, scale.height)
81 | }
82 |
83 | func xScaled(_ scale: CGFloat) -> CGFloat {
84 | x * scale
85 | }
86 |
87 | func yScaled(_ scale: CGFloat) -> CGFloat {
88 | y * scale
89 | }
90 |
91 | func clamped(min: CGFloat, max: CGFloat) -> CGPoint {
92 | .init(self.x.clamped(min: min, max: max), self.y.clamped(min: min, max: max))
93 | }
94 | }
95 |
96 | // MARK: ----- STATIC INITIALISERS
97 |
98 | public extension CGPoint {
99 |
100 | static func x(_ x: CGFloat) -> CGPoint {
101 | .init(x, 0)
102 | }
103 |
104 | static func y(_ y: CGFloat) -> CGPoint {
105 | .init(0, y)
106 | }
107 |
108 | static func point(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
109 | .init(x, y)
110 | }
111 |
112 | static func point(_ size: CGFloat) -> CGPoint {
113 | .init(size)
114 | }
115 |
116 | static func point(_ radius: CGFloat, _ angle: Angle) -> CGPoint {
117 | calcOffset(radius: radius, angle: angle)
118 | }
119 | }
120 |
121 | // MARK: ----- OPERATOR OVERLOADS
122 |
123 | public extension CGPoint {
124 |
125 | static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
126 | .init(lhs.x - rhs.x, lhs.y - rhs.y)
127 | }
128 |
129 | static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
130 | .init(lhs.x + rhs.x, lhs.y + rhs.y)
131 | }
132 | }
133 |
134 | // MARK: ----- OFFSET
135 |
136 | public extension CGPoint {
137 |
138 | func offset(_ x: CGFloat, _ y: CGFloat) -> CGPoint {
139 | CGPoint(self.x + x, self.y + y)
140 | }
141 |
142 | func offset(_ offset: CGPoint) -> CGPoint {
143 | self.offset(offset.x, offset.y)
144 | }
145 |
146 | func offset(_ offset: CGVector) -> CGPoint {
147 | self.offset(offset.dx, offset.dy)
148 | }
149 |
150 | func offset(_ offset: CGSize) -> CGPoint {
151 | self.offset(offset.width, offset.height)
152 | }
153 |
154 | func xOffset(_ xOffset: CGFloat) -> CGPoint {
155 | offset(xOffset, 0)
156 | }
157 |
158 | func yOffset(_ yOffset: CGFloat) -> CGPoint {
159 | offset(0, yOffset)
160 | }
161 |
162 | func offset(radius: CGFloat, angle: Angle) -> CGPoint {
163 | return offset(calcOffset(radius: radius, angle: angle))
164 | }
165 | }
166 |
167 | // MARK: ----- OFFSET WITH FACTOR
168 | public extension CGPoint {
169 |
170 | func offset(_ x: CGFloat, _ y: CGFloat, factor: CGFloat) -> CGPoint {
171 | return CGPoint(self.x + x * factor, self.y + y * factor)
172 | }
173 |
174 | func offset(_ offset: CGPoint, factor: CGFloat) -> CGPoint {
175 | self.offset(offset.x, offset.y, factor: factor)
176 | }
177 |
178 | func offset(_ offset: CGVector, factor: CGFloat) -> CGPoint {
179 | self.offset(offset.dx, offset.dy, factor: factor)
180 | }
181 |
182 | func offset(_ offset: CGSize, factor: CGFloat) -> CGPoint {
183 | self.offset(offset.width, offset.height, factor: factor)
184 | }
185 |
186 | func xOffset(_ xOffset: CGFloat, factor: CGFloat) -> CGPoint {
187 | offset(xOffset, 0, factor: factor)
188 | }
189 |
190 | func yOffset(_ yOffset: CGFloat, factor: CGFloat) -> CGPoint {
191 | offset(0, yOffset, factor: factor)
192 | }
193 |
194 | func offset(radius: CGFloat, angle: Angle, factor: CGFloat) -> CGPoint {
195 | return offset(calcOffset(radius: radius, angle: angle), factor: factor)
196 | }
197 | }
198 |
199 | // MARK: ----- TO WITH FACTOR
200 |
201 | public extension CGPoint {
202 |
203 | func to(_ destination: CGPoint, _ factor: CGFloat) -> CGPoint {
204 | to(destination, .square(factor))
205 | }
206 |
207 | func to(_ destination: CGPoint, _ factor: CGSize) -> CGPoint {
208 | let deltaX = destination.x - self.x
209 | let deltaY = destination.y - self.y
210 | return CGPoint(self.x + deltaX * factor.x, self.y + deltaY * factor.y)
211 | }
212 | }
213 |
214 | // MARK: ----- UTILITIES
215 |
216 | // MARK: ----- OFFSET ANCHOR
217 |
218 | public extension CGPoint {
219 |
220 | private func getOffset(in size: CGSize, anchor: UnitPoint) -> CGPoint {
221 | switch anchor {
222 | case .topLeading, .zero:
223 | return CGPoint(0, 0)
224 | case .top:
225 | return CGPoint(-size.halfWidth, 0)
226 | case .topTrailing:
227 | return CGPoint(-size.width, 0)
228 | case .leading:
229 | return CGPoint(0, -size.halfHeight)
230 | case .center:
231 | return CGPoint(-size.halfWidth, -size.halfHeight)
232 | case .trailing:
233 | return CGPoint(-size.width, -size.halfHeight)
234 | case .bottomLeading:
235 | return CGPoint(0, -size.height)
236 | case .bottom:
237 | return CGPoint(-size.halfWidth, -size.height)
238 | case .bottomTrailing:
239 | return CGPoint(-size.width, -size.height)
240 | default:
241 | return CGPoint(-(anchor.x * size.width), -(anchor.y * size.height))
242 | }
243 | }
244 |
245 | func offset(in size: CGSize, anchor: UnitPoint) -> CGPoint {
246 | self.offset(getOffset(in: size, anchor: anchor))
247 | }
248 |
249 | func offset(in point: CGPoint, anchor: UnitPoint) -> CGPoint {
250 | self.offset(in: point.asCGSize, anchor: anchor)
251 | }
252 |
253 | func offset(in vector: CGVector, anchor: UnitPoint) -> CGPoint {
254 | self.offset(in: vector.asCGSize, anchor: anchor)
255 | }
256 |
257 | func calcPoint(length: CGFloat, angle: Angle) -> CGPoint {
258 | let offset = calcOffset(radius: length, angle: angle)
259 | return CGPoint(self.x + offset.x, self.y + offset.y)
260 | }
261 | }
262 |
263 | // MARK: ----- OFFSET ANCHOR WITH FACTOR
264 |
265 | public extension CGPoint {
266 |
267 | func offset(in size: CGSize, anchor: UnitPoint, factor: CGFloat) -> CGPoint {
268 | self.offset(getOffset(in: size, anchor: anchor).scaled(factor))
269 | }
270 |
271 | func offset(in offset: CGPoint, anchor: UnitPoint, factor: CGFloat) -> CGPoint {
272 | self.offset(in: offset.asCGSize, anchor: anchor, factor: factor)
273 | }
274 |
275 | func offset(in offset: CGVector, anchor: UnitPoint, factor: CGFloat) -> CGPoint {
276 | self.offset(in: offset.asCGSize, anchor: anchor, factor: factor)
277 | }
278 |
279 | func calcPoint(length: CGFloat, angle: Angle, factor: CGFloat) -> CGPoint {
280 | calcPoint(length: length, angle: angle, factor: .square(factor))
281 | }
282 |
283 | func offset(in offset: CGSize, anchor: UnitPoint, factor: CGSize) -> CGPoint {
284 | self.offset(in: offset.scaled(factor), anchor: anchor)
285 | }
286 |
287 | func offset(in offset: CGPoint, anchor: UnitPoint, factor: CGSize) -> CGPoint {
288 | self.offset(in: offset.asCGSize, anchor: anchor, factor: factor)
289 | }
290 |
291 | func offset(in offset: CGVector, anchor: UnitPoint, factor: CGSize) -> CGPoint {
292 | self.offset(in: offset.asCGSize, anchor: anchor, factor: factor)
293 | }
294 |
295 | func calcPoint(length: CGFloat, angle: Angle, factor: CGSize) -> CGPoint {
296 | let offset = calcOffset(radius: length, angle: angle)
297 | return CGPoint(self.x + offset.x * factor.width, self.y + offset.y * factor.height)
298 | }
299 | }
300 |
301 | // MARK: ----- MOVE ORIGIN
302 |
303 | public extension CGPoint {
304 |
305 | func moveOrigin(in size: CGSize, origin: UnitPoint = .topLeading) -> CGPoint {
306 | switch origin {
307 | case .topLeading, .zero:
308 | return self
309 | case .top:
310 | return CGPoint(x + size.halfWidth, y)
311 | case .topTrailing:
312 | return CGPoint(x + size.width, y)
313 | case .leading:
314 | return CGPoint(x, y + size.halfHeight)
315 | case .center:
316 | return CGPoint(x + size.halfWidth, y + size.halfHeight)
317 | case .trailing:
318 | return CGPoint(x + size.width, y + size.halfHeight)
319 | case .bottomLeading:
320 | return CGPoint(x, y + size.height)
321 | case .bottom:
322 | return CGPoint(x + size.halfWidth, y + size.height)
323 | case .bottomTrailing:
324 | return CGPoint(x + size.width, y + size.height)
325 | default:
326 | return CGPoint(x + (origin.x * size.width), y + (origin.y * size.height))
327 | }
328 | }
329 |
330 | func moveOrigin(in size: CGPoint, origin: UnitPoint = .topLeading) -> CGPoint {
331 | moveOrigin(in: size.asCGSize, origin: origin)
332 | }
333 |
334 | func moveOrigin(in size: CGVector, origin: UnitPoint = .topLeading) -> CGPoint {
335 | moveOrigin(in: size.asCGSize, origin: origin)
336 | }
337 | }
338 |
339 | // MARK: ----- MOVE ORIGIN WITH FACTOR
340 |
341 | public extension CGPoint {
342 |
343 | func moveOrigin(in size: CGSize, origin: UnitPoint = .topLeading, factor: CGFloat) -> CGPoint {
344 | moveOrigin(in: size.scaled(factor), origin: origin)
345 | }
346 |
347 | func moveOrigin(in size: CGPoint, origin: UnitPoint = .topLeading, factor: CGFloat) -> CGPoint {
348 | moveOrigin(in: size.asCGSize, origin: origin, factor: factor)
349 | }
350 |
351 | func moveOrigin(in size: CGVector, origin: UnitPoint = .topLeading, factor: CGFloat) -> CGPoint {
352 | moveOrigin(in: size.asCGSize, origin: origin, factor: factor)
353 | }
354 |
355 | func moveOrigin(in size: CGSize, origin: UnitPoint = .topLeading, factor: CGSize) -> CGPoint {
356 | moveOrigin(in: size.scaled(factor), origin: origin)
357 | }
358 |
359 | func moveOrigin(in size: CGPoint, origin: UnitPoint = .topLeading, factor: CGSize) -> CGPoint {
360 | moveOrigin(in: size.asCGSize, origin: origin, factor: factor)
361 | }
362 |
363 | func moveOrigin(in size: CGVector, origin: UnitPoint = .topLeading, factor: CGSize) -> CGPoint {
364 | moveOrigin(in: size.asCGSize, origin: origin, factor: factor)
365 | }
366 | }
367 |
368 | // MARK: ----- CALC RADIUS AND ANGLE
369 |
370 | public extension CGPoint {
371 |
372 | func calcAngleTo(_ x: CGFloat, _ y: CGFloat) -> Angle {
373 | angleTo(CGPoint(x, y))
374 | }
375 |
376 | /**
377 | This is an expensive calculation so use with care
378 | */
379 | func radiusTo(_ point: CGPoint) -> CGFloat {
380 | sqrt(squaredRadiusTo(point))
381 | }
382 |
383 | func radiusTo(_ x: CGFloat, _ y: CGFloat) -> CGFloat {
384 | radiusTo(CGPoint(x, y))
385 | }
386 |
387 | func squaredRadiusTo(_ point: CGPoint) -> CGFloat {
388 | let delta = point - self
389 | return ((pow(delta.x, 2)) + (pow(delta.y, 2)))
390 | }
391 |
392 | func squaredRadiusTo(_ x: CGFloat, _ y: CGFloat) -> CGFloat {
393 | squaredRadiusTo(CGPoint(x, y))
394 | }
395 |
396 | func angleTo(_ point: CGPoint) -> Angle {
397 | let delta = point - self
398 | let angle = atan2(delta.y, delta.x).radians
399 | return angle
400 | }
401 |
402 | func angleTo(_ x: CGFloat, _ y: CGFloat) -> Angle {
403 | angleTo(CGPoint(x, y))
404 | }
405 | }
406 |
407 | // MARK: ----- CONVERT TO RECT
408 |
409 | public extension CGPoint {
410 |
411 | func rect(to point: CGPoint) -> CGRect {
412 | return CGRect(from: self, to: point)
413 | }
414 | }
415 |
416 | // MARK: ----- MAP FROM ONE RECT TO ANOTHER
417 |
418 | public extension CGPoint {
419 |
420 | func map(from: CGRect, to: CGRect) -> CGPoint {
421 |
422 | let xScale = from.width == 0 ? 0 : (x - from.origin.x) / from.width
423 | let yScale = from.height == 0 ? 0 : (y - from.origin.y) / from.height
424 |
425 | return CGPoint(to.relativeX(xScale), to.relativeY(yScale))
426 | }
427 | }
428 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Model/LayoutGuide/PolarLayoutCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PolarLayoutCoordinator.swift
3 | //
4 | // Created by Adam Fordyce on 03/02/2020.
5 | // Copyright © 2020 Adam Fordyce. All rights reserved.
6 | //
7 |
8 | private struct PolarLayoutCoordinator: LayoutCoordinator {
9 |
10 | let baseOrigin: CGPoint
11 | let baseRect: CGRect
12 | private let radiusCalculator: RadiusForRingCalculator
13 | private let angleCalculator: AngleForSegmentCalculator
14 | private let fromTop: Bool
15 |
16 | init(origin: CGPoint, rect: CGRect, radiusCalculator: RadiusForRingCalculator, angleCalculator: AngleForSegmentCalculator, fromTop: Bool = true) {
17 | self.baseOrigin = origin
18 | self.baseRect = rect
19 | self.radiusCalculator = radiusCalculator
20 | self.angleCalculator = angleCalculator
21 | self.fromTop = fromTop
22 | }
23 |
24 | subscript(ringIndex: Int, segmentIndex: Int) -> CGPoint {
25 | baseOrigin.offset(
26 | radius: radiusCalculator.radiusFor(ringIndex: ringIndex),
27 | angle: angleCalculator.angleFor(segmentIndex: segmentIndex).fromTopIf(fromTop))
28 | }
29 |
30 | subscript(rel relativeRadius: CGFloat, segmentIndex: Int) -> CGPoint {
31 | baseOrigin.offset(
32 | radius: radiusCalculator.radiusFor(relativeRadius: relativeRadius),
33 | angle: angleCalculator.angleFor(segmentIndex: segmentIndex).fromTopIf(fromTop))
34 | }
35 |
36 | subscript(ringIndex: Int, rel relativeAngle: CGFloat) -> CGPoint {
37 | baseOrigin.offset(
38 | radius: radiusCalculator.radiusFor(ringIndex: ringIndex),
39 | angle: angleCalculator.angleFor(relativeAngle: relativeAngle).fromTopIf(fromTop))
40 | }
41 |
42 | subscript(rel relativeRadius: CGFloat, rel relativeAngle: CGFloat) -> CGPoint {
43 | baseOrigin.offset(
44 | radius: radiusCalculator.radiusFor(relativeRadius: relativeRadius),
45 | angle: angleCalculator.angleFor(relativeAngle: relativeAngle).fromTopIf(fromTop))
46 | }
47 |
48 | var xCount: Int {
49 | radiusCalculator.ringCount
50 | }
51 |
52 | var yCount: Int {
53 | angleCalculator.segmentCount
54 | }
55 |
56 | func reframed(into rect: CGRect, originalRect: CGRect, origin: UnitPoint? = nil) -> LayoutCoordinator {
57 | let newOrigin = calcOrigin(in: rect, origin: origin ?? .center)
58 | let size: CGSize = .square(radiusCalculator.useMaxDimension ? rect.maxDimension : rect.minDimension)
59 |
60 | return PolarLayoutCoordinator(
61 | origin: newOrigin,
62 | rect: CGRect(newOrigin.offset(in: size, anchor: .center), size),
63 | radiusCalculator: radiusCalculator.reframed(rect),
64 | angleCalculator: angleCalculator.reframed(rect))
65 | }
66 |
67 | func anchorLocation(for anchor: UnitPoint, size: CGSize) -> CGPoint {
68 |
69 | if anchor == .center {
70 | return baseOrigin
71 | } else {
72 | let virtualRectOrigin = baseOrigin - baseRect.sizeScaled(0.5).asCGPoint
73 | return virtualRectOrigin.moveOrigin(in: baseRect.size, origin: anchor)
74 | }
75 | }
76 | }
77 |
78 | private extension Angle {
79 |
80 | func fromTopIf(_ condition: Bool) -> Angle {
81 | return condition ? offsetAsAngleFromTop(self) : self
82 | }
83 | }
84 |
85 | private extension PolarLayoutCoordinator {
86 |
87 | var topLeading: CGPoint {
88 | baseRect.topLeading
89 | }
90 |
91 | var top: CGPoint {
92 | baseRect.top
93 | }
94 |
95 | var topTrailing: CGPoint {
96 | baseRect.topTrailing
97 | }
98 |
99 | var trailing: CGPoint {
100 | baseRect.trailing
101 | }
102 |
103 | var bottomTrailing: CGPoint {
104 | baseRect.bottomTrailing
105 | }
106 |
107 | var bottom: CGPoint {
108 | baseRect.bottom
109 | }
110 |
111 | var bottomLeading: CGPoint {
112 | baseRect.bottomLeading
113 | }
114 |
115 | var leading: CGPoint {
116 | baseRect.leading
117 | }
118 |
119 | var center: CGPoint {
120 | baseRect.center
121 | }
122 | }
123 |
124 | // MARK: ----- POLAR CALCULATOR PROTOCOLS
125 |
126 | private protocol RadiusForRingCalculator {
127 |
128 | var radius: CGFloat {get}
129 | var useMaxDimension: Bool {get}
130 | var ringCount: Int {get}
131 | func radiusFor(ringIndex: Int) -> CGFloat
132 | func reframed(_ rect: CGRect) -> RadiusForRingCalculator
133 | }
134 |
135 | private extension RadiusForRingCalculator {
136 |
137 | func radiusFor(relativeRadius: CGFloat) -> CGFloat {
138 | radius * relativeRadius
139 | }
140 | }
141 |
142 | private protocol AngleForSegmentCalculator {
143 |
144 | var segmentCount: Int {get}
145 | func angleFor(segmentIndex: Int) -> Angle
146 | func reframed(_ rect: CGRect) -> AngleForSegmentCalculator
147 | }
148 |
149 | private extension AngleForSegmentCalculator {
150 |
151 | func angleFor(relativeAngle: CGFloat) -> Angle {
152 | .cycles(relativeAngle.asDouble)
153 | }
154 | }
155 |
156 | private struct EquidistantRadiusForRingCalculator: RadiusForRingCalculator {
157 |
158 | private typealias Config = (rings: Int, useMaxDimension: Bool)
159 |
160 | private let config: Config
161 | private let radiusStep: CGFloat
162 | fileprivate let radius: CGFloat
163 |
164 | init(_ rect: CGRect, rings: Int, useMaxDimension: Bool = false) {
165 | self.init(rect, config: (rings, useMaxDimension))
166 | }
167 |
168 | private init(_ rect: CGRect, config: Config) {
169 | self.config = (config.rings > 0 ? config.rings : 1, config.useMaxDimension)
170 | self.radius = (config.useMaxDimension ? rect.maxDimension : rect.minDimension) / 2
171 | self.radiusStep = radius / self.config.rings.asCGFloat
172 | }
173 |
174 | var ringCount: Int {
175 | config.rings
176 | }
177 |
178 | var useMaxDimension: Bool {
179 | config.useMaxDimension
180 | }
181 |
182 | func radiusFor(ringIndex: Int) -> CGFloat {
183 | radiusStep * ringIndex.asCGFloat
184 | }
185 |
186 | func reframed(_ rect: CGRect) -> RadiusForRingCalculator {
187 | EquidistantRadiusForRingCalculator(rect, config: config)
188 | }
189 | }
190 |
191 | private struct RelativeRadiusForRingCalculator: RadiusForRingCalculator {
192 |
193 | private typealias Config = (rings: [CGFloat], useMaxDimension: Bool)
194 |
195 | private let config: Config
196 | private let radiusSteps: [CGFloat]
197 | fileprivate let radius: CGFloat
198 |
199 | init(_ rect: CGRect, rings: [CGFloat], useMaxDimension: Bool = false) {
200 | self.init(rect, config: (rings, useMaxDimension))
201 | }
202 |
203 | private init(_ rect: CGRect, config: Config) {
204 | var radiusSteps: [CGFloat] = []
205 | for ring in config.rings {
206 | radiusSteps.append(ring.asCGFloat)
207 | }
208 | self.config = config
209 | self.radiusSteps = radiusSteps
210 | self.radius = (config.useMaxDimension ? rect.maxDimension : rect.minDimension) / 2
211 | }
212 |
213 | var ringCount: Int {
214 | config.rings.count
215 | }
216 |
217 | func radiusFor(ringIndex: Int) -> CGFloat {
218 | if ringIndex >= 0 && ringIndex < radiusSteps.count {
219 | return radius * radiusSteps[ringIndex]
220 | } else if ringIndex >= radiusSteps.count {
221 | return radius * (radiusSteps.last ?? 0)
222 | } else {
223 | return radius * (radiusSteps.first ?? 0)
224 | }
225 | }
226 |
227 | var useMaxDimension: Bool {
228 | config.useMaxDimension
229 | }
230 |
231 | func reframed(_ rect: CGRect) -> RadiusForRingCalculator {
232 | RelativeRadiusForRingCalculator(rect, config: config)
233 | }
234 | }
235 |
236 | private struct EquidistantAngleForSegmentCalculator: AngleForSegmentCalculator {
237 |
238 | private let segments: Int
239 | private let segmentSize: Angle
240 |
241 | init(segments: Int) {
242 | self.segments = segments > 0 ? segments : 1
243 | self.segmentSize = (360 / segments.asDouble).degrees
244 | }
245 |
246 | var segmentCount: Int {
247 | segments
248 | }
249 |
250 | func angleFor(segmentIndex: Int) -> Angle {
251 | segmentSize * segmentIndex
252 | }
253 |
254 | func reframed(_ rect: CGRect) -> AngleForSegmentCalculator {
255 | self
256 | }
257 | }
258 |
259 | private struct AbsoluteAngleForSegmentCalculator: AngleForSegmentCalculator {
260 |
261 | private let segments: [Angle]
262 |
263 | init(segments: [Angle]) {
264 | self.segments = segments
265 | }
266 |
267 | var segmentCount: Int {
268 | segments.count
269 | }
270 |
271 | func angleFor(segmentIndex: Int) -> Angle {
272 | if segmentIndex >= 0 && segmentIndex < segments.count {
273 | return segments[segmentIndex]
274 | } else if segmentIndex >= segments.count {
275 | return segments.last ?? 0.degrees
276 | } else {
277 | return segments.first ?? 0.degrees
278 | }
279 | }
280 |
281 | func reframed(_ rect: CGRect) -> AngleForSegmentCalculator {
282 | self
283 | }
284 | }
285 |
286 | // MARK: ----- LAYOUT EXTENSIONS FOR POLAR
287 |
288 | public extension LayoutGuide {
289 |
290 | private static func polarLayout(radiusCalculator: RadiusForRingCalculator, angleCalculator: AngleForSegmentCalculator, rect: CGRect, origin: UnitPoint, useMaxDimension: Bool, fromTop: Bool = true) -> LayoutGuide {
291 |
292 | let origin = calcOrigin(in: rect, origin: origin)
293 | let size: CGSize = .square(useMaxDimension ? rect.maxDimension : rect.minDimension)
294 | let coordinator = PolarLayoutCoordinator(
295 | origin: origin,
296 | rect: CGRect(origin.offset(in: size, anchor: .center), size),
297 | radiusCalculator: radiusCalculator,
298 | angleCalculator: angleCalculator, fromTop: fromTop)
299 |
300 | return LayoutGuide(coordinator, rect: rect)
301 | }
302 |
303 | /**
304 | Equidistant rings and segments
305 | */
306 | static func polar(_ rect: CGRect, rings: Int, segments: Int, useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuide {
307 |
308 | let radiusCalculator = EquidistantRadiusForRingCalculator(rect, rings: rings, useMaxDimension: useMaxDimension)
309 | let angleCalculator = EquidistantAngleForSegmentCalculator(segments: segments)
310 |
311 | return polarLayout(radiusCalculator: radiusCalculator, angleCalculator: angleCalculator, rect: rect, origin: origin, useMaxDimension: useMaxDimension, fromTop: fromTop)
312 | }
313 |
314 | /**
315 | Specified relative rings and equidistant segments
316 | */
317 | static func polar(_ rect: CGRect, rings: [CGFloat], segments: Int, useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuide {
318 |
319 | let radiusCalculator = RelativeRadiusForRingCalculator(rect, rings: rings, useMaxDimension: useMaxDimension)
320 | let angleCalculator = EquidistantAngleForSegmentCalculator(segments: segments)
321 |
322 | return polarLayout(radiusCalculator: radiusCalculator, angleCalculator: angleCalculator, rect: rect, origin: origin, useMaxDimension: useMaxDimension, fromTop: fromTop)
323 | }
324 |
325 | /**
326 | Equidistant rings and relative segments
327 | */
328 | static func polar(_ rect: CGRect, rings: Int, segments: [CGFloat], useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuide {
329 | polar(rect, rings: rings, segments: segments.map { .cycles($0.asDouble) }, useMaxDimension: useMaxDimension, origin: origin, fromTop: fromTop)
330 | }
331 |
332 | /**
333 | Equidistant rings and absolute segments
334 | */
335 | static func polar(_ rect: CGRect, rings: Int, segments: [Angle], useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuide {
336 |
337 | let radiusCalculator = EquidistantRadiusForRingCalculator(rect, rings: rings, useMaxDimension: useMaxDimension)
338 | let angleCalculator = AbsoluteAngleForSegmentCalculator(segments: segments)
339 |
340 | return polarLayout(radiusCalculator: radiusCalculator, angleCalculator: angleCalculator, rect: rect, origin: origin, useMaxDimension: useMaxDimension, fromTop: fromTop)
341 | }
342 |
343 | /**
344 | Relative rings and relative segments
345 | */
346 | static func polar(_ rect: CGRect, rings: [CGFloat], segments: [CGFloat], useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuide {
347 | polar(rect, rings: rings, segments: segments.map { .cycles($0.asDouble) }, useMaxDimension: useMaxDimension, origin: origin, fromTop: fromTop)
348 | }
349 |
350 | /**
351 | Relative rings and relative segments
352 | */
353 | static func polar(_ rect: CGRect, rings: [CGFloat], segments: [Angle], useMaxDimension: Bool = false, origin: UnitPoint = .center, fromTop: Bool = true) -> LayoutGuide {
354 |
355 | let radiusCalculator = RelativeRadiusForRingCalculator(rect, rings: rings, useMaxDimension: useMaxDimension)
356 | let angleCalculator = AbsoluteAngleForSegmentCalculator(segments: segments)
357 |
358 | return polarLayout(radiusCalculator: radiusCalculator, angleCalculator: angleCalculator, rect: rect, origin: origin, useMaxDimension: useMaxDimension, fromTop: fromTop)
359 | }
360 | }
361 |
362 |
363 |
--------------------------------------------------------------------------------
/Sources/PureSwiftUIDesign/Extensions/Convenience/SwiftUI/Path+Convenience.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Path+Convenience.swift
3 | //
4 | //
5 | // Created by Adam Fordyce on 05/11/2019.
6 | // Copyright © 2019 Adam Fordyce. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 |
12 |
13 | // MARK: ----- NORMALISATION
14 |
15 | @available(*, deprecated, message: "internal constant to PureSwiftUIDesign")
16 | public let rotationAdjustment = 90.degrees
17 |
18 | internal let rotationNormalisationAngle = 90.degrees
19 |
20 | @available(*, deprecated, renamed: "normaliseAngle")
21 | public func adjustAngle(_ angle: Angle) -> Angle {
22 | normaliseAngle(angle)
23 | }
24 |
25 |
26 | @available(*, deprecated, renamed: "offsetAsAngleFromTop")
27 | public func normaliseAngle(_ angle: Angle) -> Angle {
28 | angle - rotationNormalisationAngle
29 | }
30 |
31 | // MARK: ----- UNCONSTRAINED MOVE
32 |
33 | public extension Path {
34 |
35 | mutating func move(_ point: CGPoint) {
36 | move(to: point)
37 | }
38 |
39 | mutating func move(_ x: CGFloat, _ y: CGFloat) {
40 | move(CGPoint(x: x, y: y))
41 | }
42 |
43 | mutating func move(offset: CGPoint) {
44 | if let currentPoint = currentPoint {
45 | move(currentPoint.x + offset.x, currentPoint.y + offset.y)
46 | }
47 | }
48 |
49 | mutating func move(offset: CGSize) {
50 | move(offset: offset.asCGPoint)
51 | }
52 |
53 | mutating func move(offset: CGVector) {
54 | move(offset: offset.asCGPoint)
55 | }
56 | }
57 |
58 | // MARK: ----- CONSTRAINED MOVE IN X
59 |
60 | public extension Path {
61 |
62 | mutating func hMove(offset: CGFloat) {
63 | move(offset: CGPoint(offset, 0))
64 | }
65 | }
66 |
67 | // MARK: ----- CONSTRAINED MOVE TO X
68 |
69 | public extension Path {
70 |
71 | mutating func hMove(_ x: CGFloat) {
72 | if let currentPoint = currentPoint {
73 | move(x, currentPoint.y)
74 | }
75 | }
76 |
77 | mutating func hMove(_ point: CGPoint) {
78 | hMove(point.x)
79 | }
80 | }
81 |
82 | // MARK: ----- CONSTRAINED MOVE IN Y
83 |
84 | public extension Path {
85 |
86 | mutating func vMove(offset: CGFloat) {
87 | move(offset: CGPoint(0, offset))
88 | }
89 | }
90 |
91 | // MARK: ----- CONSTRAINED MOVE TO Y
92 |
93 | public extension Path {
94 |
95 | mutating func vMove(_ y: CGFloat) {
96 | if let currentPoint = currentPoint {
97 | move(currentPoint.x, y)
98 | }
99 | }
100 |
101 | mutating func vMove(_ point: CGPoint) {
102 | vMove(point.y)
103 | }
104 | }
105 |
106 | // MARK: ----- LINE
107 |
108 | public extension Path {
109 |
110 | mutating func line(_ point: CGPoint) {
111 | addLine(to: point)
112 | }
113 |
114 | mutating func line(length: CGFloat, angle: Angle) {
115 | if let currentPoint = currentPoint {
116 | line(currentPoint.offset(radius: length, angle: angle))
117 | }
118 | }
119 |
120 | mutating func line(_ x: CGFloat, _ y: CGFloat) {
121 | line(CGPoint(x: x, y: y))
122 | }
123 |
124 | mutating func lines(_ lines: [CGPoint]) {
125 | addLines(lines)
126 | }
127 |
128 | mutating func line(offset: CGPoint) {
129 | if let currentPoint = currentPoint {
130 | line(currentPoint.x + offset.x, currentPoint.y + offset.y)
131 | }
132 | }
133 |
134 | mutating func line(offset: CGSize) {
135 | line(offset: offset.asCGPoint)
136 | }
137 |
138 | mutating func line(offset: CGVector) {
139 | line(offset: offset.asCGPoint)
140 | }
141 | }
142 |
143 | // MARK: ----- CONSTRAINED LINE IN X
144 |
145 | public extension Path {
146 |
147 | mutating func hLine(offset: CGFloat) {
148 | line(offset: CGPoint(offset, 0))
149 | }
150 | }
151 |
152 | // MARK: ----- CONSTRAINED LINE TO X
153 |
154 | public extension Path {
155 |
156 | mutating func hLine(_ x: CGFloat) {
157 | if let currentPoint = currentPoint {
158 | line(x, currentPoint.y)
159 | }
160 | }
161 |
162 | mutating func hLine(_ point: CGPoint) {
163 | hLine(point.x)
164 | }
165 | }
166 |
167 | // MARK: ----- CONSTRAINED LINE IN Y
168 |
169 | public extension Path {
170 |
171 | mutating func vLine(offset: CGFloat) {
172 | line(offset: CGPoint(0, offset))
173 | }
174 | }
175 |
176 | // MARK: ----- CONSTRAINED LINE TO Y
177 |
178 | public extension Path {
179 |
180 | mutating func vLine(_ y: CGFloat) {
181 | if let currentPoint = currentPoint {
182 | line(currentPoint.x, y)
183 | }
184 | }
185 |
186 | mutating func vLine(_ point: CGPoint) {
187 | vLine(point.y)
188 | }
189 | }
190 |
191 | // MARK: ----- LINE AT
192 |
193 | public extension Path {
194 |
195 | mutating func line(at location: CGPoint, vector: CGVector, anchor: UnitPoint = .topLeading) {
196 | let origin = location.offset(in: vector, anchor: anchor)
197 | move(origin)
198 | line(origin.offset(vector))
199 | }
200 |
201 | mutating func hLine(at location: CGPoint, length: CGFloat, anchor: UnitPoint = .topLeading) {
202 | line(at: location, vector: CGVector(length, 0), anchor: anchor)
203 | }
204 |
205 | mutating func vLine(at location: CGPoint, length: CGFloat, anchor: UnitPoint = .topLeading) {
206 | line(at: location, vector: CGVector(0, length), anchor: anchor)
207 | }
208 | }
209 |
210 | // MARK: ----- LINE AT WITH ANGLE
211 |
212 | public extension Path {
213 |
214 | mutating func line(at location: CGPoint, length: CGFloat, angle: Angle, anchor: UnitPoint = .center) {
215 | let rotatedPoint = location.calcPoint(length: length, angle: angle)
216 | let delta = rotatedPoint - location
217 | let origin = location.offset(in: delta, anchor: anchor)
218 | line(from: origin, to: origin + delta)
219 | }
220 | }
221 |
222 | // MARK: ----- LINE FROM
223 |
224 | public extension Path {
225 |
226 | mutating func line(from: CGPoint, to: CGPoint) {
227 | move(from)
228 | line(to)
229 | }
230 | }
231 |
232 | // MARK: ----- LINE FROM WITH ANGLE
233 |
234 | public extension Path {
235 |
236 | mutating func line(from: CGPoint, length: CGFloat, angle: Angle) {
237 | line(from: from, to: from.calcPoint(length: length, angle: angle))
238 | }
239 | }
240 |
241 | // MARK: ----- RECT
242 |
243 | public extension Path {
244 |
245 | mutating func rect(_ rect: CGRect, transform: CGAffineTransform = .identity) {
246 | addRect(rect, transform: transform)
247 | }
248 |
249 | mutating func rect(_ origin: CGPoint, _ size: CGSize, transform: CGAffineTransform = .identity) {
250 | rect(CGRect(origin, size), transform: transform)
251 | }
252 |
253 | mutating func rects(_ rects: [CGRect], transform: CGAffineTransform = .identity) {
254 | addRects(rects, transform: transform)
255 | }
256 | }
257 |
258 | // MARK: ----- RECT WITH ANCHOR
259 |
260 | public extension Path {
261 |
262 | mutating func rect(_ rect: CGRect, anchor: UnitPoint, transform: CGAffineTransform = .identity) {
263 | self.rect(rect.offset(anchor: anchor), transform: transform)
264 | }
265 |
266 | mutating func rect(_ origin: CGPoint, _ size: CGSize, anchor: UnitPoint, transform: CGAffineTransform = .identity) {
267 | rect(CGRect(origin.offset(in: size, anchor: anchor), size), transform: transform)
268 | }
269 | }
270 |
271 | // MARK: ----- FROM POINT TO POINT
272 |
273 | public extension Path {
274 |
275 | mutating func rect(from: CGPoint, to: CGPoint, anchor: UnitPoint = .topLeading, transform: CGAffineTransform = .identity) {
276 | rect(CGRect(from: from, to: to), transform: transform)
277 | }
278 | }
279 |
280 | // MARK: ----- ROUNDED RECT WITH CORNER SIZE
281 |
282 | public extension Path {
283 |
284 | mutating func roundedRect(_ rect: CGRect, cornerSize: CGSize, transform: CGAffineTransform = .identity) {
285 | addRoundedRect(in: rect, cornerSize: cornerSize, transform: transform)
286 | }
287 |
288 | mutating func roundedRect(_ origin: CGPoint, _ size: CGSize, cornerSize: CGSize, transform: CGAffineTransform = .identity) {
289 | roundedRect(CGRect(origin, size), cornerSize: cornerSize, transform: transform)
290 | }
291 | }
292 |
293 | // MARK: ----- ROUNDED RECT WITH CORNER RADIUS
294 |
295 | public extension Path {
296 |
297 | mutating func roundedRect(_ rect: CGRect, cornerRadius: CGFloat, transform: CGAffineTransform = .identity) {
298 | addRoundedRect(in: rect, cornerSize: .square(cornerRadius), transform: transform)
299 | }
300 |
301 | mutating func roundedRect(_ origin: CGPoint, _ size: CGSize, cornerRadius: CGFloat, transform: CGAffineTransform = .identity) {
302 | roundedRect(CGRect(origin, size), cornerRadius: cornerRadius, transform: transform)
303 | }
304 | }
305 |
306 | // MARK: ----- ROUNDED RECT WITH CORNER SIZE AND ANCHOR
307 |
308 | public extension Path {
309 |
310 | mutating func roundedRect(_ rect: CGRect, cornerSize: CGSize, anchor: UnitPoint, transform: CGAffineTransform = .identity) {
311 | roundedRect(rect.offset(anchor: anchor), cornerSize: cornerSize, transform: transform)
312 | }
313 |
314 | mutating func roundedRect(_ origin: CGPoint, _ size: CGSize, cornerSize: CGSize, anchor: UnitPoint, transform: CGAffineTransform = .identity) {
315 | roundedRect(CGRect(origin.offset(in: size, anchor: anchor), size), cornerSize: cornerSize, transform: transform)
316 | }
317 | }
318 |
319 | // MARK: ----- ROUNDED RECT AT WITH CORNER RADIUS AND ANCHOR
320 |
321 | public extension Path {
322 |
323 | mutating func roundedRect(_ rect: CGRect, cornerRadius: CGFloat, anchor: UnitPoint, transform: CGAffineTransform = .identity) {
324 | roundedRect(rect.offset(anchor: anchor), cornerSize: .square(cornerRadius), transform: transform)
325 | }
326 |
327 | mutating func roundedRect(_ origin: CGPoint, _ size: CGSize, cornerRadius: CGFloat, anchor: UnitPoint, transform: CGAffineTransform = .identity) {
328 | roundedRect(CGRect(origin.offset(in: size, anchor: anchor), size), cornerSize: .square(cornerRadius), transform: transform)
329 | }
330 | }
331 |
332 | // MARK: ----- ELLIPSE
333 |
334 | public extension Path {
335 |
336 | mutating func ellipse(_ rect: CGRect, transform: CGAffineTransform = .identity) {
337 | addEllipse(in: rect, transform: transform)
338 | }
339 |
340 | mutating func ellipse(_ origin: CGPoint, _ size: CGSize, transform: CGAffineTransform = .identity) {
341 | ellipse(CGRect(origin, size), transform: transform)
342 | }
343 | }
344 |
345 | // MARK: ----- ELLIPSE WITH ANCHOR
346 |
347 | public extension Path {
348 |
349 | mutating func ellipse(_ rect: CGRect, anchor: UnitPoint, transform: CGAffineTransform = .identity) {
350 | ellipse(rect.offset(anchor: anchor), transform: transform)
351 | }
352 |
353 | mutating func ellipse(_ origin: CGPoint, _ size: CGSize, anchor: UnitPoint, transform: CGAffineTransform = .identity) {
354 | ellipse(CGRect(origin.offset(in: size, anchor: anchor), size), transform: transform)
355 | }
356 |
357 | }
358 |
359 | // MARK: ----- CIRCLE
360 |
361 | public extension Path {
362 |
363 | mutating func circle(_ rect: CGRect, useMaxDimension: Bool = false, transform: CGAffineTransform = .identity) {
364 | circle(rect.center, diameter: useMaxDimension ? rect.maxDimension : rect.minDimension, transform: transform)
365 | }
366 |
367 | mutating func circle(_ origin: CGPoint, radius: CGFloat, transform: CGAffineTransform = .identity) {
368 | circle(origin, diameter: radius * 2, transform: transform)
369 | }
370 |
371 | mutating func circle(_ origin: CGPoint, diameter: CGFloat, transform: CGAffineTransform = .identity) {
372 | circle(origin, diameter: diameter, anchor: .center, transform: transform)
373 | }
374 | }
375 |
376 | // MARK: ----- CIRCLE WITH ANCHOR
377 |
378 | public extension Path {
379 |
380 | mutating func circle(_ rect: CGRect, useMaxDimension: Bool = false, anchor: UnitPoint, transform: CGAffineTransform = .identity) {
381 | circle(rect.center, diameter: useMaxDimension ? rect.maxDimension : rect.minDimension, anchor: anchor, transform: transform)
382 | }
383 |
384 | mutating func circle(_ origin: CGPoint, radius: CGFloat, anchor: UnitPoint, transform: CGAffineTransform = .identity) {
385 | circle(origin, diameter: radius * 2, anchor: anchor, transform: transform)
386 | }
387 |
388 | mutating func circle(_ origin: CGPoint, diameter: CGFloat, anchor: UnitPoint, transform: CGAffineTransform = .identity) {
389 | let squareCGSize: CGSize = .square(diameter)
390 | addEllipse(in: CGRect(origin.offset(in: squareCGSize, anchor: anchor), squareCGSize), transform: transform)
391 | }
392 | }
393 |
394 | // MARK: ----- QUAD CURVE
395 |
396 | private let controlPointRadius: CGFloat = 3
397 |
398 | public extension Path {
399 |
400 | mutating func quadCurve(_ point: CGPoint, cp: CGPoint, showControlPoints: Bool = false) {
401 | if showControlPoints {
402 | addControlPoint(cp)
403 | }
404 | addQuadCurve(to: point, control: cp)
405 | }
406 | }
407 |
408 | // MARK: ----- CURVE
409 |
410 | public extension Path {
411 |
412 | private mutating func addControlPoint(_ cp: CGPoint) {
413 | if let currentPoint = currentPoint {
414 |
415 | let angleToCp = currentPoint.angleTo(cp)
416 | let radiusToCp = currentPoint.radiusTo(cp)
417 |
418 | line(length: radiusToCp - controlPointRadius, angle: angleToCp)
419 | arc(cp, radius: controlPointRadius, startAngle: angleToCp - 180.degrees, delta: 360.degrees)
420 | line(currentPoint)
421 | }
422 | }
423 |
424 | mutating func curve(_ point: CGPoint, cp1: CGPoint, cp2: CGPoint, showControlPoints: Bool = false) {
425 | if showControlPoints {
426 | addControlPoint(cp1)
427 | }
428 | addCurve(to: point, control1: cp1, control2: cp2)
429 | if showControlPoints {
430 | addControlPoint(cp2)
431 | }
432 | }
433 | }
434 |
435 | // MARK: ----- ARC
436 |
437 | public extension Path {
438 |
439 | mutating func arc(_ center: CGPoint, radius: CGFloat, startAngle: Angle, endAngle: Angle, clockwise: Bool = true, transform: CGAffineTransform = .identity) {
440 | addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: !clockwise, transform: transform)
441 | }
442 |
443 | mutating func arc(_ center: CGPoint, radius: CGFloat, startAngle: Angle, delta: Angle, transform: CGAffineTransform = .identity) {
444 | addRelativeArc(center: center, radius: radius, startAngle: startAngle, delta: delta, transform: transform)
445 | }
446 |
447 | mutating func arc(_ tangent1End: CGPoint, _ tangent2End: CGPoint, radius: CGFloat, transform: CGAffineTransform = .identity) {
448 | addArc(tangent1End: tangent1End, tangent2End: tangent2End, radius: radius, transform: transform)
449 | }
450 |
451 | // with normalised angles
452 |
453 | mutating func arc(_ center: CGPoint, radius: CGFloat, startAngleFromTop: Angle, endAngleFromTop: Angle, clockwise: Bool = true, transform: CGAffineTransform = .identity) {
454 | arc(center, radius: radius, startAngle: offsetAsAngleFromTop(startAngleFromTop), endAngle: offsetAsAngleFromTop(endAngleFromTop), clockwise: clockwise, transform: transform)
455 | }
456 |
457 | mutating func arc(_ center: CGPoint, radius: CGFloat, startAngleFromTop: Angle, delta: Angle, transform: CGAffineTransform = .identity) {
458 | addRelativeArc(center: center, radius: radius, startAngle: offsetAsAngleFromTop(startAngleFromTop), delta: delta, transform: transform)
459 | }
460 | }
461 |
--------------------------------------------------------------------------------