Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import ComponentsKit
import SwiftUI
import UIKit

struct ProgressBarPreview: View {
@State private var model = ProgressBarVM()
@State private var currentValue: CGFloat = 50

var body: some View {
VStack {
PreviewWrapper(title: "SwiftUI") {
SUProgressBar(currentValue: $currentValue, model: model)
}
Form {
ComponentColorPicker(selection: self.$model.color)
Picker("Current Value", selection: $currentValue) {
Text("0").tag(CGFloat(0))
Text("25").tag(CGFloat(25))
Text("50").tag(CGFloat(50))
Text("75").tag(CGFloat(75))
Text("100").tag(CGFloat(100))
}
SizePicker(selection: self.$model.size)
Picker("Style", selection: $model.style) {
Text("Light").tag(ProgressBarVM.ProgressBarStyle.light)
Text("Filled").tag(ProgressBarVM.ProgressBarStyle.filled)
Text("Striped").tag(ProgressBarVM.ProgressBarStyle.striped)
}
}
}
}
}

#Preview {
ProgressBarPreview()
}
3 changes: 3 additions & 0 deletions Examples/DemosApp/DemosApp/Core/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ struct App: View {
NavigationLinkWithTitle("Loading") {
LoadingPreview()
}
NavigationLinkWithTitle("Progress Bar") {
ProgressBarPreview()
}
NavigationLinkWithTitle("Radio Group") {
RadioGroupPreview()
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/ComponentsKit/ProgressBar/Models/ProgressBarStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

extension ProgressBarVM {
/// Defines the visual styles for the progress bar component.
public enum ProgressBarStyle {
case light
case filled
case striped
}
}
140 changes: 140 additions & 0 deletions Sources/ComponentsKit/ProgressBar/Models/ProgressBarVM.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import SwiftUI

/// A model that defines the appearance properties for a a progress bar component.
public struct ProgressBarVM: ComponentVM {
/// The color of the progress bar..
///
/// Defaults to `.primary`.
public var color: ComponentColor = .primary

/// The visual style of the progress bar component.
///
/// Defaults to `.light`.
public var style: ProgressBarStyle = .striped

/// The size of the progress bar.
///
/// Defaults to `.medium`.
public var size: ComponentSize = .large

/// The minimum value of the progress bar.
public var minValue: CGFloat = 0

/// The maximum value of the progress bar.
public var maxValue: CGFloat = 100

/// The corner radius of the progress bar.
///
/// Defaults to `.medium`.
public var cornerRadius: ComponentRadius = .medium

/// The width of the stripes in the `.striped` progress bar style.
public var stripeWidth: CGFloat = 2

/// The spacing between the stripes in the `.striped` progress bar style.
public var stripeSpacing: CGFloat = 4

/// The angle of the stripes in the `.striped` progress bar style..
public var stripeAngle: Angle = .degrees(135)

/// Initializes a new instance of `ProgressBarVM` with default values.
public init() {}
}

// MARK: - Shared Helpers

extension ProgressBarVM {
var barHeight: CGFloat {
switch style {
case .light:
switch size {
case .small:
return 4
case .medium:
return 8
case .large:
return 16
}
case .filled, .striped:
switch size {
case .small:
return 10
case .medium:
return 25
case .large:
return 45
}
}
}

var computedCornerRadius: CGFloat {
switch style {
case .light:
return barHeight / 2
case .filled, .striped:
switch size {
case .small, .medium:
return barHeight / 2
case .large:
return barHeight / 2.5
}
}
}

var backgroundColor: UniversalColor {
switch style {
case .light:
return self.color.main.withOpacity(0.15)
case .filled, .striped:
return self.color.main
}
}

var barColor: UniversalColor {
switch style {
case .light:
return self.color.main
case .filled, .striped:
return self.color.contrast
}
}

func shouldUpdateLayout(_ oldModel: Self) -> Bool {
return self.size != oldModel.size
}

public func stripesPath(in rect: CGRect) -> Path {
var path = Path()
let step = stripeWidth + stripeSpacing
let radians = stripeAngle.radians
let dx = rect.height * tan(radians)

for x in stride(from: dx - step, through: rect.width + step, by: step) {
let topLeft = CGPoint(x: x, y: 0)
let topRight = CGPoint(x: x + stripeWidth, y: 0)
let bottomLeft = CGPoint(x: x + dx, y: rect.height)
let bottomRight = CGPoint(x: x + stripeWidth + dx, y: rect.height)

path.move(to: topLeft)
path.addLine(to: topRight)
path.addLine(to: bottomRight)
path.addLine(to: bottomLeft)
path.closeSubpath()
}

return path
}

public func stripesBezierPath(in rect: CGRect) -> UIBezierPath {
let swiftUIPath = stripesPath(in: rect)
return UIBezierPath(cgPath: swiftUIPath.cgPath)
}
}

// MARK: - Validation

extension ProgressBarVM {
public func isValid() -> Bool {
return minValue < maxValue
}
}
99 changes: 99 additions & 0 deletions Sources/ComponentsKit/ProgressBar/SUProgressBar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import SwiftUI

/// A SwiftUI component that displays a progress bar.
public struct SUProgressBar: View {
// MARK: - Properties

/// A model that defines the appearance properties.
public var model: ProgressBarVM

@Binding private var currentValue: CGFloat

// MARK: - Initializer

/// Initializer.
/// - Parameters:
/// - currentValue: A binding to the current value.
/// - model: A model that defines the appearance properties.
public init(
currentValue: Binding<CGFloat>,
model: ProgressBarVM
) {
self._currentValue = currentValue
self.model = model
}

// MARK: - Body

public var body: some View {
GeometryReader { geometry in
switch model.style {
case .light:
HStack(spacing: 4) {
Rectangle()
.foregroundColor(model.barColor.color)
.frame(width: geometry.size.width * fraction, height: model.barHeight)
.cornerRadius(model.computedCornerRadius)
Rectangle()
.foregroundColor(model.backgroundColor.color)
.frame(width: geometry.size.width * (1 - fraction), height: model.barHeight)
.cornerRadius(model.computedCornerRadius)
.scaleEffect(fraction == 0 ? 0 : 1, anchor: .leading)
}
.animation(.spring, value: fraction)

case .filled:
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: model.computedCornerRadius)
.foregroundColor(model.color.main.color)
.frame(width: geometry.size.width, height: model.barHeight)

RoundedRectangle(cornerRadius: model.computedCornerRadius)
.foregroundColor((model.color.contrast ?? .foreground).color)
.frame(width: (geometry.size.width - 6) * fraction, height: model.barHeight - 6)
.padding(3)
.scaleEffect(fraction == 0 ? 0 : 1, anchor: .leading)
}
.animation(.spring, value: fraction)

case .striped:
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: model.computedCornerRadius)
.foregroundColor(model.color.main.color)
.frame(width: geometry.size.width, height: model.barHeight)

RoundedRectangle(cornerRadius: model.computedCornerRadius)
.foregroundColor(model.color.contrast.color)
.frame(width: (geometry.size.width - 6) * fraction, height: model.barHeight - 6)
.padding(3)
.scaleEffect(fraction == 0 ? 0 : 1, anchor: .leading)

StripesShape(model: model)
.foregroundColor(model.color.main.color)
.scaleEffect(1.2)
.cornerRadius(model.computedCornerRadius)
.clipped()
}
.animation(.spring, value: fraction)
}
}
.frame(height: model.barHeight)
}

// MARK: - Properties

private var fraction: CGFloat {
let range = model.maxValue - model.minValue
guard range != 0 else { return 0 }
let fraction = (currentValue - model.minValue) / range
return max(0, min(1, fraction))
}
}

struct StripesShape: Shape {
var model: ProgressBarVM

func path(in rect: CGRect) -> Path {
model.stripesPath(in: rect)
}
}
Loading