Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e506856
UKProgressBar
VislovIvan Dec 30, 2024
484b972
Documentation
VislovIvan Dec 30, 2024
d9b9b81
swiftlint fix
VislovIvan Dec 30, 2024
5bafbbc
renamed layoutNeedsUpdate
VislovIvan Dec 30, 2024
34c6d87
some fix
VislovIvan Dec 30, 2024
99c9756
fix shouldUpdateLayout
VislovIvan Jan 2, 2025
ab22fe9
Preview fix
VislovIvan Jan 2, 2025
dc192aa
progress extracted
VislovIvan Jan 2, 2025
2779d70
updateBarWidth fix
VislovIvan Jan 2, 2025
3c963fa
intrinsicContentSize fix
VislovIvan Jan 2, 2025
ace276a
currentValue renamed to initialValue
VislovIvan Jan 2, 2025
9fcef79
horizontalPadding
VislovIvan Jan 2, 2025
49b8e91
corner radius fix
VislovIvan Jan 2, 2025
76516e9
renaming
VislovIvan Jan 2, 2025
faf4e54
style extension for UKProgressBar
VislovIvan Jan 2, 2025
50bdc32
swiftlint fix
VislovIvan Jan 2, 2025
78c07c7
fix layout metod
VislovIvan Jan 5, 2025
d921d2a
CAShapeLayer for stripedLayer instead of UIView
VislovIvan Jan 5, 2025
84abff2
fix animation
VislovIvan Jan 5, 2025
5f90099
code style fix
VislovIvan Jan 5, 2025
9a2c02e
some fix
VislovIvan Jan 7, 2025
1a7b716
Merge branch 'SUProgressBar' into UKProgressBar
VislovIvan Jan 7, 2025
058e95a
swiftlint fix
VislovIvan Jan 7, 2025
9d2ba44
Merge branch 'UKProgressBar' of https://github.com/componentskit/Comp…
VislovIvan Jan 7, 2025
cb9babd
fix merge conflicts
VislovIvan Jan 7, 2025
0d1b482
move ProgressBar folder into Components
mikhailChelbaev Jan 7, 2025
263a302
improve layout in UKProgressBar
mikhailChelbaev Jan 7, 2025
4e22f5b
fix progress bar preview
mikhailChelbaev Jan 7, 2025
c10bc32
improve helpers to calculate corner radius in progress bar
mikhailChelbaev Jan 7, 2025
fcc588d
simplify code in progress bar
mikhailChelbaev Jan 7, 2025
f8fab4d
improve method's name
mikhailChelbaev Jan 7, 2025
7ae6885
validate min max values in UKProgressBar
mikhailChelbaev Jan 8, 2025
06e613b
change animation style in progress bar to `linear`
mikhailChelbaev Jan 8, 2025
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
Expand Up @@ -11,6 +11,9 @@ struct ProgressBarPreview: View {

var body: some View {
VStack {
PreviewWrapper(title: "UIKit") {
UKProgressBarRepresentable(currentValue: $currentValue, model: self.model)
}
PreviewWrapper(title: "SwiftUI") {
SUProgressBar(currentValue: $currentValue, model: self.model)
}
Expand All @@ -37,6 +40,25 @@ struct ProgressBarPreview: View {
}
}

struct UKProgressBarRepresentable: UIViewRepresentable {
@Binding var currentValue: CGFloat
var model: ProgressBarVM

func makeUIView(context: Context) -> UKProgressBar {
let progressBar = UKProgressBar(currentValue: currentValue, model: model)
return progressBar
}

func updateUIView(_ uiView: UKProgressBar, context: Context) {
uiView.currentValue = currentValue
uiView.model = model
uiView.setNeedsLayout()
}

static func dismantleUIView(_ uiView: UKProgressBar, coordinator: ()) {
}
}

#Preview {
ProgressBarPreview()
}
4 changes: 3 additions & 1 deletion Sources/ComponentsKit/ProgressBar/Models/ProgressBarVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,10 @@ extension ProgressBarVM {
}
}

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

private func stripesCGPath(in rect: CGRect) -> CGMutablePath {
Expand Down
17 changes: 12 additions & 5 deletions Sources/ComponentsKit/ProgressBar/SUProgressBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ public struct SUProgressBar: View {
.foregroundStyle(self.model.backgroundColor.color)
.frame(width: geometry.size.width * (1 - self.progress), height: self.model.barHeight)
}
.animation(.spring, value: self.progress)

.animation(
Animation.easeInOut(duration: 0.3),
value: self.progress
)
case .filled:
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: self.model.computedCornerRadius)
Expand All @@ -62,8 +64,10 @@ public struct SUProgressBar: View {
.padding(.vertical, self.model.contentPaddings.top)
.padding(.horizontal, self.model.contentPaddings.trailing)
}
.animation(.spring, value: self.progress)

.animation(
Animation.easeInOut(duration: 0.3),
value: self.progress
)
case .striped:
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: self.model.computedCornerRadius)
Expand All @@ -81,7 +85,10 @@ public struct SUProgressBar: View {
.cornerRadius(self.model.computedCornerRadius)
.clipped()
}
.animation(.spring, value: self.progress)
.animation(
Animation.easeInOut(duration: 0.3),
value: self.progress
)
}
}
.frame(height: self.model.barHeight)
Expand Down
223 changes: 223 additions & 0 deletions Sources/ComponentsKit/ProgressBar/UKProgressBar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import AutoLayout
import UIKit

/// A UIKit component that displays a Progress Bar.
open class UKProgressBar: UIView, UKComponent {
// MARK: - Properties

/// A model that defines the appearance properties.
public var model: ProgressBarVM {
didSet {
self.update(oldValue)
}
}

/// The current progress value for the progress bar.
public var currentValue: CGFloat {
didSet {
self.updateBarWidth()
}
}

// MARK: - Subviews

/// A view representing the part of the progress bar that is not yet filled.
private let remainingView = UIView()

/// A view representing the filled part of the bar.
private let filledView = UIView()

/// A view used to display the striped pattern.
private let stripedView = UIView()

// MARK: - Layout Constraints

private var remainingViewConstraints: LayoutConstraints = .init()
private var filledViewConstraints: LayoutConstraints = .init()
private var stripedViewConstraints: LayoutConstraints = .init()

// MARK: - Private Properties

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

// MARK: - Initialization

/// Initializer.
/// - Parameters:
/// - currentValue: The initial progress value. Defaults to `0`.
/// - model: A model that defines the appearance properties.
public init(
currentValue: CGFloat = 0,
model: ProgressBarVM = .init()
) {
self.currentValue = currentValue
self.model = model
super.init(frame: .zero)

self.setup()
self.style()
self.layout()
}

public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Setup

private func setup() {
self.addSubview(self.remainingView)
self.addSubview(self.filledView)
self.addSubview(self.stripedView)
}

// MARK: - Style

private func style() {
switch self.model.style {
case .light:
self.remainingView.backgroundColor = self.model.backgroundColor.uiColor
self.filledView.backgroundColor = self.model.barColor.uiColor
self.stripedView.backgroundColor = .clear

case .filled:
self.remainingView.backgroundColor = self.model.color.main.uiColor
self.filledView.backgroundColor = self.model.color.contrast.uiColor
self.stripedView.backgroundColor = .clear

case .striped:
self.remainingView.backgroundColor = self.model.color.main.uiColor
self.filledView.backgroundColor = self.model.color.contrast.uiColor
self.stripedView.backgroundColor = self.model.color.main.uiColor
}
}

// MARK: - Layout

private func layout() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method always must be called only once, so here you should set only initial constraints

when the layout changes, you can update constrains in the update method

self.remainingViewConstraints.deactivateAll()
self.filledViewConstraints.deactivateAll()
self.stripedViewConstraints.deactivateAll()

UIView.performWithoutAnimation {
switch self.model.style {
case .light:
self.remainingViewConstraints = .merged {
self.remainingView.after(self.filledView, padding: 4)
self.remainingView.centerVertically()
self.remainingView.height(self.model.barHeight)
self.remainingView.width(0)
}
self.filledViewConstraints = .merged {
self.filledView.leading(0)
self.filledView.centerVertically()
self.filledView.height(self.model.barHeight)
self.filledView.width(0)
}

case .filled, .striped:
self.remainingViewConstraints = .merged {
self.remainingView.horizontally(0)
self.remainingView.centerVertically()
self.remainingView.height(self.model.barHeight)
}
self.filledViewConstraints = .merged {
self.filledView.leading(3, to: self.remainingView)
self.filledView.vertically(3, to: self.remainingView)
self.filledView.width(0)
}

if self.model.style == .striped {
self.stripedViewConstraints = .merged {
self.stripedView.horizontally(0)
self.stripedView.centerVertically()
self.stripedView.height(self.model.barHeight)
}
}
}
}

self.setNeedsLayout()
}

// MARK: - Update

public func update(_ oldModel: ProgressBarVM) {
guard self.model != oldModel else { return }

self.style()

if self.model.shouldUpdateLayout(from: oldModel) {
self.layout()
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}

self.updateBarWidth()
}

private func updateBarWidth() {
let duration: TimeInterval = 0.3

UIView.performWithoutAnimation {
self.layoutIfNeeded()
}

switch self.model.style {
case .light:
let totalWidth = self.bounds.width - 4
let filledWidth = totalWidth * self.progress
self.filledViewConstraints.width?.constant = max(0, filledWidth)
self.remainingViewConstraints.width?.constant = max(0, totalWidth - filledWidth)

case .filled, .striped:
let totalWidth = self.bounds.width - 6
let filledWidth = totalWidth * self.progress
self.filledViewConstraints.width?.constant = max(0, filledWidth)
}

UIView.animate(withDuration: duration) {
self.layoutIfNeeded()
}
}

// MARK: - Style Helpers

open override func layoutSubviews() {
super.layoutSubviews()

switch self.model.style {
case .light:
self.remainingView.layer.cornerRadius = self.model.computedCornerRadius
self.filledView.layer.cornerRadius = self.model.computedCornerRadius

case .filled:
self.remainingView.layer.cornerRadius = self.model.computedCornerRadius
self.filledView.layer.cornerRadius = self.model.innerCornerRadius

case .striped:
self.remainingView.layer.cornerRadius = self.model.computedCornerRadius
self.filledView.layer.cornerRadius = self.model.innerCornerRadius
self.stripedView.layer.cornerRadius = self.model.computedCornerRadius
self.stripedView.clipsToBounds = true
}

self.updateBarWidth()
self.layoutIfNeeded()

if self.model.style == .striped {
let shapeLayer = CAShapeLayer()
shapeLayer.path = self.model.stripesBezierPath(in: self.stripedView.bounds).cgPath
self.stripedView.layer.mask = shapeLayer
}
}

open override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: self.model.barHeight)
}
}
Loading