├── .github └── FUNDING.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Gifs └── example.gif ├── Images ├── ColorOne.png ├── ColorTwo.png ├── Fill.png ├── FontOne.png ├── FontTwo.png ├── LineCap.png ├── LineWidth.png ├── ShowBottomText.png └── ShowText.png ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── CircularProgress │ └── CircularProgress.swift └── Tests ├── CircularProgressTests ├── CircularProgressTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: arnavmotwani 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Gifs/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnavMotwani/CircularProgressSwiftUI/017efe091152bad991a3a4c935a444facb0ed3d3/Gifs/example.gif -------------------------------------------------------------------------------- /Images/ColorOne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnavMotwani/CircularProgressSwiftUI/017efe091152bad991a3a4c935a444facb0ed3d3/Images/ColorOne.png -------------------------------------------------------------------------------- /Images/ColorTwo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnavMotwani/CircularProgressSwiftUI/017efe091152bad991a3a4c935a444facb0ed3d3/Images/ColorTwo.png -------------------------------------------------------------------------------- /Images/Fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnavMotwani/CircularProgressSwiftUI/017efe091152bad991a3a4c935a444facb0ed3d3/Images/Fill.png -------------------------------------------------------------------------------- /Images/FontOne.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnavMotwani/CircularProgressSwiftUI/017efe091152bad991a3a4c935a444facb0ed3d3/Images/FontOne.png -------------------------------------------------------------------------------- /Images/FontTwo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnavMotwani/CircularProgressSwiftUI/017efe091152bad991a3a4c935a444facb0ed3d3/Images/FontTwo.png -------------------------------------------------------------------------------- /Images/LineCap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnavMotwani/CircularProgressSwiftUI/017efe091152bad991a3a4c935a444facb0ed3d3/Images/LineCap.png -------------------------------------------------------------------------------- /Images/LineWidth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnavMotwani/CircularProgressSwiftUI/017efe091152bad991a3a4c935a444facb0ed3d3/Images/LineWidth.png -------------------------------------------------------------------------------- /Images/ShowBottomText.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnavMotwani/CircularProgressSwiftUI/017efe091152bad991a3a4c935a444facb0ed3d3/Images/ShowBottomText.png -------------------------------------------------------------------------------- /Images/ShowText.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArnavMotwani/CircularProgressSwiftUI/017efe091152bad991a3a4c935a444facb0ed3d3/Images/ShowText.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Arnav Motwani 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 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: "CircularProgress", 8 | platforms: [ 9 | .iOS(.v15), 10 | .macOS(.v12) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "CircularProgress", 16 | targets: ["CircularProgress"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "CircularProgress", 27 | dependencies: []), 28 | .testTarget( 29 | name: "CircularProgressTests", 30 | dependencies: ["CircularProgress"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CircularProgress 2 | 3 | SwiftUI package that creates an animated circular progress bar 4 | 5 | ### Installation: It requires at least iOS 15, iPadOS 15, macOS 12 and Xcode 13! 6 | 7 | In Xcode go to `File -> Add Packages...` and paste in the repo's url: `https://github.com/ArnavMotwani/CircularProgressSwiftUI.git` then either select a version or the main branch 8 | 9 | I will update the main branch more frequently with minor changes, while the version will only increase with significant changes. 10 | 11 | There also a branch called iOS13 which supports iOS 13+ and macOS 10_15+ 12 | 13 | ## Usage: 14 | 15 | Import the package into the file with `import CircularProgress` 16 | 17 | ### Example: 18 | Here is how the default view, with no customizations, can be implemented 19 | 20 |

21 | 22 |

23 | 24 | ```swift 25 | import SwiftUI 26 | import CircularProgress 27 | 28 | struct ContentView: View { 29 | @State var count = 0 30 | let total = 10 31 | var progress: CGFloat{ 32 | return CGFloat(count)/CGFloat(total) 33 | } 34 | var body: some View { 35 | VStack { 36 | CircularProgressView(count: count, total: total, progress: progress) 37 | .padding(50) 38 | HStack{ 39 | Button("Decrease", action: {self.count -= 1}) 40 | Spacer() 41 | Button("Increase", action: {self.count += 1}) 42 | } 43 | .padding(50) 44 | } 45 | } 46 | } 47 | 48 | ``` 49 | ## Fill Customization: 50 | The Progress Bar can be filled with a Linear or an Angular Gradient. By default the fill is `LinearGradient(gradient: Gradient(colors: [Color.green, Color.blue]), startPoint: .top, endPoint: .bottom)` however you can pass a custom Linear or Angular Gradient to the fill Parameter. 51 | 52 | ## Parameters: 53 | 54 | | parameter | optional? | type | description | default | 55 | |----------------|-----------|-----------------------------------|-------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------| 56 | | count | required | Int | Current value (larger text in the centre) | - | 57 | | total | required | Int | Total value (smaller text in the centre) | - | 58 | | progress | required | CGFloat | Progress of the bar | - | 59 | | fontOne | optional | Font | Font of larger text in the centre | Font.system(size: 75, weight: .bold, design: .rounded) | 60 | | fontTwo | optional | Font | Font of smaller text in the centre | Font.system(size: 25, weight: .bold, design: .rounded) | 61 | | colorOne | optional | Color | Color of larger text in the centre | Color.primary | 62 | | colorTwo | optional | Color | Color of smaller text in the centre | Color.gray | 63 | | fill | optional | LinearGradient or AngularGradient | Fill of the progress bar | LinearGradient(gradient: Gradient(colors: [Color.green, Color.blue]), startPoint: .top, endPoint: .bottom) | 64 | | lineWidth | optional | CGFloat | Width of the progress bar | 25.0 | 65 | | lineCap | optional | CGLineCap | The line cap at the end of the progress bar | CGLineCap.round | 66 | | showText | optional | Bool | Choose whether the text at the centre is displayed or not | true | 67 | | showBottomText | optional | Bool | Choose whether the bottom text in the centre is visible (if showText is true) | true | 68 | 69 | ### Examples 70 | 71 | #### fontOne 72 |

73 | 74 |

75 | 76 | ```swift 77 | CircularProgressView(count: count, total: total, progress: progress, fontOne: Font.title.bold()) 78 | ``` 79 | --- 80 | #### fontTwo 81 |

82 | 83 |

84 | 85 | ```swift 86 | CircularProgressView(count: count, total: total, progress: progress, fontTwo: Font.title2) 87 | ``` 88 | --- 89 | #### colorOne 90 |

91 | 92 |

93 | 94 | ```swift 95 | CircularProgressView(count: count, total: total, progress: progress, colorOne: Color.blue) 96 | ``` 97 | --- 98 | #### colorTwo 99 |

100 | 101 |

102 | 103 | ```swift 104 | CircularProgressView(count: count, total: total, progress: progress, colorTwo: Color.blue) 105 | ``` 106 | --- 107 | #### fill 108 |

109 | 110 |

111 | 112 | ```swift 113 | CircularProgressView(count: count, total: total, progress: progress, fill: LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing)) 114 | ``` 115 | --- 116 | #### lineWidth 117 |

118 | 119 |

120 | 121 | ```swift 122 | CircularProgressView(count: count, total: total, progress: progress, lineWidth: 50) 123 | ``` 124 | --- 125 | #### lineCap 126 |

127 | 128 |

129 | 130 | ```swift 131 | CircularProgressView(count: count, total: total, progress: progress, lineCap: CGLineCap.square) 132 | ``` 133 | --- 134 | #### showText 135 |

136 | 137 |

138 | 139 | ```swift 140 | CircularProgressView(count: count, total: total, progress: progress, showText: false) 141 | ``` 142 | --- 143 | #### showBottomText 144 |

145 | 146 |

147 | 148 | ```swift 149 | CircularProgressView(count: count, total: total, progress: progress, showBottomText: false) 150 | } 151 | ``` 152 | 153 | 154 | -------------------------------------------------------------------------------- /Sources/CircularProgress/CircularProgress.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct CircularProgressView: View { 4 | 5 | //MARK: Required variables 6 | var count: Int 7 | var total: Int 8 | var progress: CGFloat 9 | 10 | //MARK: Optional variables 11 | //fontOne for the current value text and fontTwo for the total value text in the centre. 12 | var fontOne: Font 13 | var fontTwo: Font 14 | 15 | //colorOne for the current value text and colorTwo for the total value text in the centre. 16 | var colorOne: Color 17 | var colorTwo: Color 18 | 19 | //The fill variable is used to choose the gradient inside the progress bar 20 | var fill: AnyShapeStyle 21 | //The lineWidth variable is used to choose the width of the progress bar (Not the enter view) 22 | var lineWidth: CGFloat 23 | //The lineCap variable is used to choose the line caps at the end of the progress bar 24 | var lineCap: CGLineCap 25 | //Choose whether the text in the centre is shown. 26 | var showText: Bool 27 | //Choose whether the bottom text in the centre of the progress bar is shown. 28 | var showBottomText: Bool 29 | 30 | //MARK: Init 31 | //Declared to allow view access the package 32 | //Also sets defaults for optional variables 33 | public init(count: Int, 34 | total: Int, 35 | progress: CGFloat, 36 | fontOne: Font = Font.system(size: 75, weight: .bold, design: .rounded), 37 | fontTwo: Font = Font.system(size: 25, weight: .bold, design: .rounded), 38 | colorOne: Color = Color.primary, 39 | colorTwo: Color = Color.gray, 40 | fill: LinearGradient = LinearGradient(gradient: Gradient(colors: [Color.green, Color.blue]), startPoint: .top, endPoint: .bottom), 41 | lineWidth: CGFloat = 25.0, 42 | lineCap: CGLineCap = CGLineCap.round, 43 | showText: Bool = true, 44 | showBottomText: Bool = true) { 45 | 46 | self.count = count 47 | self.total = total 48 | self.progress = progress 49 | self.fontOne = fontOne 50 | self.fontTwo = fontTwo 51 | self.colorOne = colorOne 52 | self.colorTwo = colorTwo 53 | self.fill = AnyShapeStyle(fill) 54 | self.lineWidth = lineWidth 55 | self.lineCap = lineCap 56 | self.showText = showText 57 | self.showBottomText = showBottomText 58 | } 59 | 60 | public init(count: Int, 61 | total: Int, 62 | progress: CGFloat, 63 | fontOne: Font = Font.system(size: 75, weight: .bold, design: .rounded), 64 | fontTwo: Font = Font.system(size: 25, weight: .bold, design: .rounded), 65 | colorOne: Color = Color.primary, 66 | colorTwo: Color = Color.gray, 67 | fill: AngularGradient, 68 | lineWidth: CGFloat = 25.0, 69 | lineCap: CGLineCap = CGLineCap.round, 70 | showText: Bool = true, 71 | showBottomText: Bool = true) { 72 | 73 | self.count = count 74 | self.total = total 75 | self.progress = progress 76 | self.fontOne = fontOne 77 | self.fontTwo = fontTwo 78 | self.colorOne = colorOne 79 | self.colorTwo = colorTwo 80 | self.fill = AnyShapeStyle(fill) 81 | self.lineWidth = lineWidth 82 | self.lineCap = lineCap 83 | self.showText = showText 84 | self.showBottomText = showBottomText 85 | } 86 | 87 | //MARK: View 88 | public var body: some View { 89 | ZStack{ 90 | //Background line for progress 91 | Circle() 92 | .stroke(lineWidth: lineWidth) 93 | .opacity(0.3) 94 | .foregroundColor(Color.secondary) 95 | 96 | //Trimmed circle to represent progress 97 | Circle() 98 | .trim(from: 0.0, to: CGFloat(min(self.progress, 1.0))) 99 | .stroke(fill ,style: StrokeStyle(lineWidth: lineWidth, lineCap: lineCap, lineJoin: .round)) 100 | .rotationEffect(Angle(degrees: 270.0)) 101 | .animation(.linear, value: progress) 102 | 103 | if showText { 104 | //Text at the centre 105 | VStack { 106 | //Text for current value 107 | Text("\(count)") 108 | .font(fontOne) 109 | .foregroundColor(colorOne) 110 | if showBottomText{ 111 | //Text for total value 112 | Text("/ \(total)") 113 | .font(fontTwo) 114 | .foregroundColor(colorTwo) 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Tests/CircularProgressTests/CircularProgressTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftUI 3 | @testable import CircularProgress 4 | 5 | final class CircularProgressTests: XCTestCase { 6 | func testLinear() { 7 | let item = CircularProgressView(count: 5, total: 10, progress: 0.5) 8 | XCTAssertEqual(item.progress, 0.5) 9 | } 10 | func testAngular() { 11 | let item = CircularProgressView(count: 5, total: 10, progress: 0.5, fill: AngularGradient(colors: [.blue], center: .center)) 12 | XCTAssertEqual(item.progress, 0.5) 13 | } 14 | 15 | static var allTests = [ 16 | ("testLinear", testLinear), 17 | ("testAngular", testAngular) 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /Tests/CircularProgressTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(CircularProgressTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import CircularProgressTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += CircularProgressTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------