Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,42 @@
import ComponentsKit
import SwiftUI
import UIKit

struct ProgressBarPreview: View {
@State private var model = ProgressBarVM()
@State private var currentValue: CGFloat = 0
private let timer = Timer
.publish(every: 0.1, on: .main, in: .common)
.autoconnect()

var body: some View {
VStack {
PreviewWrapper(title: "SwiftUI") {
SUProgressBar(currentValue: self.$currentValue, model: self.model)
}
Form {
ComponentColorPicker(selection: self.$model.color)
ComponentRadiusPicker(selection: self.$model.cornerRadius) {
Text("Custom: 2px").tag(ComponentRadius.custom(2))
}
SizePicker(selection: self.$model.size)
Picker("Style", selection: self.$model.style) {
Text("Light").tag(ProgressBarVM.Style.light)
Text("Filled").tag(ProgressBarVM.Style.filled)
Text("Striped").tag(ProgressBarVM.Style.striped)
}
}
}
.onReceive(self.timer) { _ in
if self.currentValue < self.model.maxValue {
self.currentValue += (self.model.maxValue - self.model.minValue) / 100
} else {
self.currentValue = self.model.minValue
}
}
}
}

#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 @@ -29,6 +29,9 @@ struct App: View {
NavigationLinkWithTitle("Loading") {
LoadingPreview()
}
NavigationLinkWithTitle("Progress Bar") {
ProgressBarPreview()
}
NavigationLinkWithTitle("Modal (Bottom)") {
BottomModalPreview()
}
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 Style {
case light
case filled
case striped
}
}
148 changes: 148 additions & 0 deletions Sources/ComponentsKit/ProgressBar/Models/ProgressBarVM.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
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 `.accent`.
public var color: ComponentColor = .accent

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

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

/// 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

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

// MARK: - Shared Helpers

extension ProgressBarVM {
var innerBarPadding: CGFloat {
return 3
}

var barHeight: CGFloat {
switch self.style {
case .light:
switch size {
case .small:
return 4
case .medium:
return 8
case .large:
return 12
}
case .filled, .striped:
switch self.size {
case .small:
return 20
case .medium:
return 32
case .large:
return 42
}
}
}

var computedCornerRadius: CGFloat {
switch self.cornerRadius {
case .none:
return 0.0
case .small:
return self.barHeight / 3.5
case .medium:
return self.barHeight / 3.0
case .large:
return self.barHeight / 2.5
case .full:
return self.barHeight / 2.0
case .custom(let value):
return min(value, self.barHeight / 2)
}
}

var innerCornerRadius: CGFloat {
return max(0, self.computedCornerRadius - self.innerBarPadding)
}

var backgroundColor: UniversalColor {
switch style {
case .light:
return self.color.background
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
}

private func stripesCGPath(in rect: CGRect) -> CGMutablePath {
let stripeWidth: CGFloat = 2
let stripeSpacing: CGFloat = 4
let stripeAngle: Angle = .degrees(135)

let path = CGMutablePath()
let step = stripeWidth + stripeSpacing
let radians = stripeAngle.radians
let dx = rect.height * tan(radians)
for x in stride(from: dx, through: rect.width + rect.height, 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 stripesPath(in rect: CGRect) -> Path {
return Path(self.stripesCGPath(in: rect))
}

public func stripesBezierPath(in rect: CGRect) -> UIBezierPath {
return UIBezierPath(cgPath: self.stripesCGPath(in: rect))
}
}

// MARK: - Validation

extension ProgressBarVM {
func validateMinMaxValues() {
if self.minValue > self.maxValue {
assertionFailure("Min value must be less than max value")
}
}
}
100 changes: 100 additions & 0 deletions Sources/ComponentsKit/ProgressBar/SUProgressBar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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
/// A binding to control the current value.
@Binding public var currentValue: CGFloat

private var progress: CGFloat {
let range = self.model.maxValue - self.model.minValue

guard range > 0 else {
return 0
}

let progress = (self.currentValue - self.model.minValue) / range
return max(0, min(1, progress))
}

// 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 = .init()
) {
self._currentValue = currentValue
self.model = model
}

// MARK: - Body

public var body: some View {
GeometryReader { geometry in
switch self.model.style {
case .light:
HStack(spacing: 4) {
RoundedRectangle(cornerRadius: self.model.computedCornerRadius)
.foregroundStyle(self.model.barColor.color)
.frame(width: geometry.size.width * self.progress)
RoundedRectangle(cornerRadius: self.model.computedCornerRadius)
.foregroundStyle(self.model.backgroundColor.color)
.frame(width: geometry.size.width * (1 - self.progress))
}

case .filled:
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: self.model.computedCornerRadius)
.foregroundStyle(self.model.color.main.color)
.frame(width: geometry.size.width)

RoundedRectangle(cornerRadius: self.model.innerCornerRadius)
.foregroundStyle(self.model.color.contrast.color)
.frame(width: (geometry.size.width - self.model.innerBarPadding * 2) * self.progress)
.padding(.vertical, self.model.innerBarPadding)
.padding(.horizontal, self.model.innerBarPadding)
}

case .striped:
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: self.model.computedCornerRadius)
.foregroundStyle(self.model.color.main.color)
.frame(width: geometry.size.width)

RoundedRectangle(cornerRadius: self.model.innerCornerRadius)
.foregroundStyle(self.model.color.contrast.color)
.frame(width: (geometry.size.width - self.model.innerBarPadding * 2) * self.progress)
.padding(.vertical, self.model.innerBarPadding)
.padding(.horizontal, self.model.innerBarPadding)

StripesShape(model: self.model)
.foregroundStyle(self.model.color.main.color)
.cornerRadius(self.model.computedCornerRadius)
.clipped()
}
}
}
.animation(.spring, value: self.progress)
.frame(height: self.model.barHeight)
.onAppear {
self.model.validateMinMaxValues()
}
}
}

// MARK: - Helpers

struct StripesShape: Shape {
var model: ProgressBarVM

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