├── 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 | Polar Layout Guides 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 | --------------------------------------------------------------------------------