├── .gitignore ├── Resources ├── Bar.gif ├── Circle.gif └── Ring.gif ├── Tests ├── LinuxMain.swift └── GCProgressViewTests │ ├── XCTestManifests.swift │ └── GCProgressViewTests.swift ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Sources └── GCProgressView │ ├── GCProgressViewStyle.swift │ ├── GCProgressBar.swift │ ├── GCProgressRing.swift │ ├── GCProgressCircle.swift │ └── GCProgressView.swift ├── README.md ├── LICENSE └── Package.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /Resources/Bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graycampbell/GCProgressView/HEAD/Resources/Bar.gif -------------------------------------------------------------------------------- /Resources/Circle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graycampbell/GCProgressView/HEAD/Resources/Circle.gif -------------------------------------------------------------------------------- /Resources/Ring.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graycampbell/GCProgressView/HEAD/Resources/Ring.gif -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import GCProgressViewTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += GCProgressViewTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/GCProgressViewTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(GCProgressViewTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/GCProgressViewTests/GCProgressViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import GCProgressView 3 | 4 | final class GCProgressViewTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | 11 | static var allTests = [ 12 | ("testExample", testExample), 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Sources/GCProgressView/GCProgressViewStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GCProgressViewStyle.swift 3 | // 4 | // 5 | // Created by Gray Campbell on 5/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension GCProgressView { 11 | 12 | /// The styles permitted for the progress view. 13 | 14 | enum Style: String, CaseIterable { 15 | 16 | /// A horizontal bar that animates from left to right. 17 | 18 | case bar 19 | 20 | /// A circle that animates clockwise. 21 | 22 | case circle 23 | 24 | /// A ring that animates clockwise. 25 | 26 | case ring 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GCProgressView 2 | 3 |

4 | 5 | 6 | 7 |

8 | 9 | ## Installation 10 | 11 | `GCProgressView` is available as a [Swift Package](https://developer.apple.com/documentation/swift_packages). You can find information on how to add package dependencies to your app [here](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app). 12 | 13 | ## Usage 14 | 15 | ```swift 16 | import GCProgressView 17 | ``` 18 | 19 | ```swift 20 | GCProgressView(style: .bar, progress: self.$progress) 21 | GCProgressView(style: .circle, progress: self.$progress) 22 | GCProgressView(style: .ring, progress: self.$progress) 23 | ``` 24 | -------------------------------------------------------------------------------- /Sources/GCProgressView/GCProgressBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GCProgressBar.swift 3 | // 4 | // 5 | // Created by Gray Campbell on 5/11/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GCProgressBar: Shape { 11 | 12 | // MARK: Properties 13 | 14 | var progress: Double 15 | 16 | var animatableData: Double { 17 | get { self.progress } 18 | set { self.progress = newValue } 19 | } 20 | 21 | // MARK: Path 22 | 23 | func path(in rect: CGRect) -> Path { 24 | guard self.progress > .zero else { 25 | return Path() 26 | } 27 | 28 | let startPoint = CGPoint(x: rect.minX, y: rect.midY) 29 | let endPoint = CGPoint(x: rect.size.width * CGFloat(self.progress), y: rect.midY) 30 | 31 | var path = Path() 32 | 33 | path.move(to: startPoint) 34 | path.addLine(to: endPoint) 35 | 36 | return path 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gray Campbell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/GCProgressView/GCProgressRing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GCProgressRing.swift 3 | // 4 | // 5 | // Created by Gray Campbell on 5/11/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GCProgressRing: Shape { 11 | 12 | // MARK: Properties 13 | 14 | var progress: Double 15 | 16 | var animatableData: Double { 17 | get { self.progress } 18 | set { self.progress = newValue } 19 | } 20 | 21 | // MARK: Path 22 | 23 | func path(in rect: CGRect) -> Path { 24 | guard self.progress > .zero else { 25 | return Path() 26 | } 27 | 28 | let center = CGPoint(x: rect.midX, y: rect.midY) 29 | let radius = min(rect.size.width, rect.size.height) / 2 30 | let startAngle = Angle.degrees(-90) 31 | let endAngle = startAngle + .degrees(360 * self.progress) 32 | 33 | var path = Path() 34 | 35 | path.addArc(center: center, 36 | radius: radius, 37 | startAngle: startAngle, 38 | endAngle: endAngle, 39 | clockwise: false) 40 | 41 | return path 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/GCProgressView/GCProgressCircle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GCProgressCircle.swift 3 | // 4 | // 5 | // Created by Gray Campbell on 5/11/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GCProgressCircle: Shape { 11 | 12 | // MARK: Properties 13 | 14 | var progress: Double 15 | 16 | var animatableData: Double { 17 | get { self.progress } 18 | set { self.progress = newValue } 19 | } 20 | 21 | // MARK: Path 22 | 23 | func path(in rect: CGRect) -> Path { 24 | guard self.progress > .zero else { 25 | return Path() 26 | } 27 | 28 | let center = CGPoint(x: rect.midX, y: rect.midY) 29 | let radius = min(rect.size.width, rect.size.height) / 2 30 | let startAngle = Angle.degrees(-90) 31 | let endAngle = startAngle + .degrees(360 * self.progress) 32 | 33 | var path = Path() 34 | 35 | path.move(to: center) 36 | path.addArc(center: center, 37 | radius: radius, 38 | startAngle: startAngle, 39 | endAngle: endAngle, 40 | clockwise: false) 41 | 42 | return path 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 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: "GCProgressView", 8 | platforms: [ 9 | .iOS(.v13), .macOS(.v10_10) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library( 14 | name: "GCProgressView", 15 | targets: ["GCProgressView"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target( 25 | name: "GCProgressView", 26 | dependencies: []), 27 | .testTarget( 28 | name: "GCProgressViewTests", 29 | dependencies: ["GCProgressView"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /Sources/GCProgressView/GCProgressView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GCProgressView.swift 3 | // 4 | // 5 | // Created by Gray Campbell on 5/11/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A view that depicts the progress of a task over time. 11 | 12 | public struct GCProgressView: View { 13 | 14 | // MARK: Properties 15 | 16 | /// The progress view style. 17 | 18 | public let style: GCProgressView.Style 19 | 20 | /// The current progress shown by the progress view. 21 | /// 22 | /// The current progress is represented by a `Double` value between 0.0 and 1.0, inclusive, where 1.0 indicates the completion of the task. Values less than 0.0 and greater than 1.0 are pinned to those limits. 23 | 24 | @Binding public var progress: Double 25 | 26 | private var adjustedProgress: Double { 27 | min(max(0, self.progress), 1) 28 | } 29 | 30 | // MARK: Initializers 31 | 32 | /// Creates an instance that displays the progress of a task over time. 33 | /// 34 | /// - Parameter style: The progress view style. 35 | /// - Parameter progress: A binding indicating the progress of a task. 36 | /// 37 | /// - Returns: A progress view that displays the progress of a task over time. 38 | 39 | public init(style: GCProgressView.Style, progress: Binding) { 40 | self.style = style 41 | self._progress = progress 42 | } 43 | 44 | // MARK: Body 45 | 46 | public var body: some View { 47 | GeometryReader { geometry in 48 | if self.style == .bar { 49 | GCProgressBar(progress: self.adjustedProgress) 50 | .stroke(style: self.barStrokeStyle(geometry)) 51 | .background( 52 | GCProgressBar(progress: 1) 53 | .stroke(style: self.barStrokeStyle(geometry)) 54 | .foregroundColor(Color(.systemFill)) 55 | ) 56 | .padding(.horizontal, self.barPadding(geometry)) 57 | } 58 | else if self.style == .circle { 59 | GCProgressCircle(progress: self.adjustedProgress) 60 | .fill() 61 | .background( 62 | GCProgressCircle(progress: 1) 63 | .fill(Color(.systemFill)) 64 | ) 65 | } 66 | else { 67 | GCProgressRing(progress: self.adjustedProgress) 68 | .stroke(style: self.ringStrokeStyle(geometry)) 69 | .background( 70 | GCProgressRing(progress: 1) 71 | .stroke(style: self.ringStrokeStyle(geometry)) 72 | .foregroundColor(Color(.systemFill)) 73 | ) 74 | .padding(self.ringPadding(geometry)) 75 | } 76 | } 77 | .animation(.linear) 78 | } 79 | } 80 | 81 | // MARK: - Padding, Line Width, & Stroke Style 82 | 83 | extension GCProgressView { 84 | 85 | // MARK: Bar 86 | 87 | private func barPadding(_ geometry: GeometryProxy) -> CGFloat { 88 | self.barLineWidth(geometry) / 2 89 | } 90 | 91 | private func barLineWidth(_ geometry: GeometryProxy) -> CGFloat { 92 | min(geometry.size.width / 8, geometry.size.height) 93 | } 94 | 95 | private func barStrokeStyle(_ geometry: GeometryProxy) -> StrokeStyle { 96 | StrokeStyle(lineWidth: self.barLineWidth(geometry), lineCap: .round) 97 | } 98 | 99 | // MARK: Ring 100 | 101 | private func ringPadding(_ geometry: GeometryProxy) -> CGFloat { 102 | self.ringLineWidth(geometry) / 2 103 | } 104 | 105 | private func ringLineWidth(_ geometry: GeometryProxy) -> CGFloat { 106 | let smallestDimension = min(geometry.size.width, geometry.size.height) 107 | 108 | return max(smallestDimension / 8, 1) 109 | } 110 | 111 | private func ringStrokeStyle(_ geometry: GeometryProxy) -> StrokeStyle { 112 | StrokeStyle(lineWidth: self.ringLineWidth(geometry), lineCap: .round) 113 | } 114 | } 115 | --------------------------------------------------------------------------------