├── .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 |
--------------------------------------------------------------------------------