├── .gitignore ├── Sources ├── progress-demo.gif └── SwiftProgress │ ├── CircularProgress.swift │ └── LinearProgress.swift ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /Sources/progress-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnesKaraosman/SwiftProgress/HEAD/Sources/progress-demo.gif -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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: "SwiftProgress", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_14), 11 | .tvOS(.v13), 12 | .watchOS(.v6) 13 | ], 14 | products: [ 15 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 16 | .library( 17 | name: "SwiftProgress", 18 | targets: ["SwiftProgress"]), 19 | ], 20 | dependencies: [ 21 | // Dependencies declare other packages that this package depends on. 22 | // .package(url: /* package url */, from: "1.0.0"), 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 26 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 27 | .target( 28 | name: "SwiftProgress", 29 | dependencies: []) 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /Sources/SwiftProgress/CircularProgress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircularProgress.swift 3 | // 4 | // 5 | // Created by Enes Karaosman on 13.04.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct CircularProgress: Animatable, View { 11 | 12 | /// Between 0 - 100 13 | private var progress: CGFloat 14 | private let lineWidth: CGFloat 15 | private var foregroundColor: Color? 16 | private let backgroundColor: Color 17 | private var gradient: LinearGradient? 18 | 19 | public var animatableData: Double { 20 | get { 21 | return Double(progress) 22 | } 23 | set { 24 | progress = CGFloat(newValue) 25 | } 26 | } 27 | 28 | private var overlay: AnyView { 29 | if self.foregroundColor != nil { 30 | return AnyView( 31 | RingShape(progress: progress, lineWidth: lineWidth) 32 | .foregroundColor(self.foregroundColor) 33 | ) 34 | } else { 35 | return AnyView( 36 | self.gradient!.clipShape(RingShape(progress: progress, lineWidth: lineWidth)) 37 | ) 38 | } 39 | } 40 | 41 | public init(progress: CGFloat, lineWidth: CGFloat, foregroundColor: Color, backgroundColor: Color = .clear) { 42 | self.progress = progress 43 | self.lineWidth = lineWidth 44 | self.foregroundColor = foregroundColor 45 | self.backgroundColor = backgroundColor 46 | } 47 | 48 | public init(progress: CGFloat, lineWidth: CGFloat, gradient: LinearGradient, backgroundColor: Color = .clear) { 49 | self.progress = progress 50 | self.lineWidth = lineWidth 51 | self.gradient = gradient 52 | self.backgroundColor = backgroundColor 53 | } 54 | 55 | public var body: some View { 56 | ZStack { 57 | RingShape(progress: 100, lineWidth: lineWidth) 58 | .foregroundColor(backgroundColor) 59 | 60 | RingShape(progress: progress, lineWidth: lineWidth) 61 | .overlay(overlay) 62 | } 63 | } 64 | 65 | } 66 | 67 | public struct RingShape: Shape { 68 | 69 | /// Between 0 - 100 70 | public let progress: CGFloat 71 | public let lineWidth: CGFloat 72 | 73 | public func path(in rect: CGRect) -> Path { 74 | 75 | let center = CGPoint(x: rect.midX, y: rect.midY) 76 | let radius = rect.midX - lineWidth 77 | 78 | var path = Path() 79 | 80 | path.addArc( 81 | center: center, 82 | radius: radius, 83 | startAngle: .degrees(0), 84 | endAngle: .degrees(3.6 * Double(progress)), 85 | clockwise: false 86 | ) 87 | 88 | return path.strokedPath(.init(lineWidth: lineWidth, lineCap: .round, lineJoin: .round)) 89 | 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /Sources/SwiftProgress/LinearProgress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinearProgress.swift 3 | // 4 | // 5 | // Created by Enes Karaosman on 13.04.2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct LinearProgress: Animatable, View { 11 | 12 | public enum FillAxis { 13 | case horizontal 14 | case vertical 15 | } 16 | 17 | /// Between 0 - 100 18 | private var progress: CGFloat 19 | 20 | private let cornerRadius: CGFloat 21 | private let backgroundColor: Color 22 | private var foregroundColor: Color? 23 | private var gradient: LinearGradient? 24 | private let fillAxis: FillAxis 25 | 26 | public var animatableData: Double { 27 | get { 28 | return Double(progress) 29 | } 30 | set { 31 | progress = CGFloat(newValue) 32 | } 33 | } 34 | 35 | private var overlay: AnyView { 36 | if self.foregroundColor != nil { 37 | return AnyView( Rectangle().foregroundColor(self.foregroundColor) ) 38 | } else { 39 | return AnyView( self.gradient! ) 40 | } 41 | } 42 | 43 | public init(progress: CGFloat, foregroundColor: Color, backgroundColor: Color = .clear, cornerRadius: CGFloat = 8, fillAxis: FillAxis = .horizontal) { 44 | self.progress = progress 45 | self.backgroundColor = backgroundColor 46 | self.foregroundColor = foregroundColor 47 | self.cornerRadius = cornerRadius 48 | self.fillAxis = fillAxis 49 | } 50 | 51 | public init(progress: CGFloat, gradient: LinearGradient, backgroundColor: Color = .clear, cornerRadius: CGFloat = 8, fillAxis: FillAxis = .horizontal) { 52 | self.progress = progress 53 | self.backgroundColor = backgroundColor 54 | self.gradient = gradient 55 | self.cornerRadius = cornerRadius 56 | self.fillAxis = fillAxis 57 | } 58 | 59 | private func needsToBeFilledArea(totalArea: CGFloat) -> CGFloat { 60 | return totalArea * (100 - self.progress) / 100 61 | } 62 | 63 | private func calculateOffset(totalArea: CGSize) -> CGSize { 64 | if self.fillAxis == .horizontal { 65 | return CGSize( 66 | width: -self.needsToBeFilledArea(totalArea: totalArea.width), 67 | height: 0 68 | ) 69 | } 70 | return CGSize( 71 | width: 0, 72 | height: self.needsToBeFilledArea(totalArea: totalArea.height) 73 | ) 74 | } 75 | 76 | public var body: some View { 77 | 78 | GeometryReader { geometry in 79 | 80 | Rectangle().foregroundColor(self.backgroundColor) 81 | .overlay( 82 | self.overlay 83 | .offset(self.calculateOffset(totalArea: geometry.size)) 84 | ) 85 | .clipShape(Rectangle()) 86 | .cornerRadius(self.cornerRadius) 87 | 88 | } 89 | 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftProgress 2 | 3 | LinearProgress bar & CircularProgress bar for SwiftUI. 4 | 5 | **Demo GIF**
6 | 7 | ![](https://github.com/EnesKaraosman/SwiftProgress/blob/master/Sources/progress-demo.gif) 8 | 9 | ### Installation 10 | 11 | #### Swift Package Manager 12 | 13 | Add via https://github.com/EnesKaraosman/SwiftProgress.git 14 | 15 | ### Usage 16 | 17 | #### LinearProgress 18 | 19 | ```swift 20 | // Available constructors 21 | 22 | LinearProgress( 23 | progress: CGFloat, 24 | foregroundColor: Color, 25 | backgroundColor: Color = .clear, 26 | cornerRadius: CGFloat = 8 27 | ) 28 | 29 | LinearProgress( 30 | progress: CGFloat, 31 | gradient: LinearGradient, 32 | backgroundColor: Color = .clear, 33 | cornerRadius: CGFloat = 8 34 | ) 35 | ``` 36 | 37 | #### CircularProgress 38 | 39 | ```swift 40 | // Available constructors 41 | 42 | CircularProgress( 43 | progress: CGFloat, 44 | lineWidth: CGFloat, 45 | foregroundColor: Color, 46 | backgroundColor: Color = .clear, 47 | fillAxis: FillAxis = .horizontal // vertical & horizontal options 48 | ) 49 | 50 | CircularProgress( 51 | progress: CGFloat, 52 | lineWidth: CGFloat, 53 | gradient: LinearGradient, 54 | backgroundColor: Color = .clear, 55 | fillAxis: FillAxis = .horizontal // vertical & horizontal options 56 | ) 57 | ``` 58 | 59 | #### Full Example (reflects the demo GIF) 60 | 61 | ```swift 62 | @State private var fillPercentage: CGFloat = 20 63 | 64 | var body: some View { 65 | 66 | 67 | VStack { 68 | 69 | LinearProgress( 70 | progress: self.fillPercentage, 71 | foregroundColor: .green, 72 | backgroundColor: Color.green.opacity(0.2), 73 | fillAxis: .vertical 74 | ) 75 | .frame(width: 40, height: 100) 76 | 77 | LinearProgress( 78 | progress: self.fillPercentage, 79 | gradient: LinearGradient( 80 | gradient: .init(colors: [.yellow, .red]), 81 | startPoint: .leading, 82 | endPoint: .trailing 83 | ), 84 | backgroundColor: Color.blue.opacity(0.2), 85 | cornerRadius: 16 86 | ) 87 | .frame(height: 50) 88 | .padding() 89 | 90 | LinearProgress(progress: self.fillPercentage, foregroundColor: Color.green.opacity(0.3)) 91 | .clipShape(Capsule()) 92 | .frame(height: 50) 93 | .padding() 94 | 95 | ZStack { 96 | LinearProgress(progress: self.fillPercentage, foregroundColor: .blue, cornerRadius: 0) 97 | .frame(height: 50) 98 | .padding() 99 | 100 | Text(String(format: "%1.f", self.fillPercentage)) 101 | .font(.title) 102 | .foregroundColor(.pink) 103 | .shadow(radius: 4) 104 | } 105 | 106 | 107 | Slider(value: self.$fillPercentage, in: 0...100) 108 | .padding() 109 | 110 | HStack { 111 | CircularProgress(currentPercentage: self.fillPercentage, lineWidth: 8, foregroundColor: .orange) 112 | .rotationEffect(.degrees(-90)) 113 | 114 | ZStack { 115 | CircularProgress( 116 | currentPercentage: self.fillPercentage, 117 | lineWidth: 16, 118 | gradient: LinearGradient( 119 | gradient: .init(colors: [.yellow, .red]), 120 | startPoint: .leading, 121 | endPoint: .trailing 122 | ), 123 | backgroundColor: .gray 124 | ) 125 | 126 | Text(String(format: "%1.f", self.fillPercentage)) 127 | .font(.title) 128 | .foregroundColor(.orange) 129 | .shadow(radius: 8) 130 | } 131 | }.frame(height: 200) 132 | 133 | } 134 | } 135 | ``` 136 | --------------------------------------------------------------------------------