From 45f376749962b98912e9e6e66401e283cafcaac2 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Thu, 23 Jan 2025 17:52:56 +0300 Subject: [PATCH 1/8] UKSlider --- .../PreviewPages/SliderPreview.swift | 4 + .../Components/Slider/Models/SliderVM.swift | 4 +- .../Components/Slider/UKSlider.swift | 234 ++++++++++++++++++ 3 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 Sources/ComponentsKit/Components/Slider/UKSlider.swift diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift index 99c4df99..14310684 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift @@ -7,6 +7,10 @@ struct SliderPreview: View { var body: some View { VStack { + PreviewWrapper(title: "UIKit") { + UKSlider(model: self.model) + .preview + } PreviewWrapper(title: "SwiftUI") { SUSlider(currentValue: self.$currentValue, model: self.model) } diff --git a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift index 18bd5900..20b3baa2 100644 --- a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift +++ b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift @@ -137,7 +137,7 @@ extension SliderVM { max(self.handleSize.height, self.trackHeight) } - private func sliderWidth(for totalWidth: CGFloat) -> CGFloat { + func sliderWidth(for totalWidth: CGFloat) -> CGFloat { max(0, totalWidth - self.handleSize.width - 2 * self.trackSpacing) } @@ -163,7 +163,7 @@ extension SliderVM { func shouldUpdateLayout(_ oldModel: Self) -> Bool { return self.style != oldModel.style || self.size != oldModel.size || - self.step != oldModel.step + self.step != oldModel.step || } } diff --git a/Sources/ComponentsKit/Components/Slider/UKSlider.swift b/Sources/ComponentsKit/Components/Slider/UKSlider.swift new file mode 100644 index 00000000..f9783254 --- /dev/null +++ b/Sources/ComponentsKit/Components/Slider/UKSlider.swift @@ -0,0 +1,234 @@ +import AutoLayout +import UIKit + +/// A UIKit component that displays a slider. +open class UKSlider: UIView, UKComponent { + // MARK: - Properties + + public var model: SliderVM { + didSet { + self.update(oldValue) + } + } + + public var currentValue: CGFloat { + didSet { + self.updateProgressWidthAndAppearance() + } + } + + private var initialProgressWidthOnDragBegan: CGFloat = 0 + + // MARK: - Subviews + + public let backgroundView = UIView() + public let progressView = UIView() + public let stripedLayer = CAShapeLayer() + + public let handleView = UIView() + + // MARK: - Layout Constraints + + private var backgroundViewLightLeadingConstraint: NSLayoutConstraint? + private var backgroundViewFilledLeadingConstraint: NSLayoutConstraint? + private var progressViewConstraints: LayoutConstraints = .init() + private var handleConstraints: LayoutConstraints = .init() + + // MARK: - Private Properties + + private var progress: CGFloat { + self.model.progress(for: self.currentValue) + } + + // MARK: - UIView + + open override var intrinsicContentSize: CGSize { + return self.sizeThatFits(UIView.layoutFittingExpandedSize) + } + + // MARK: - Initialization + + public init( + initialValue: CGFloat = 50, + model: SliderVM = .init() + ) { + self.currentValue = initialValue + 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.backgroundView) + self.addSubview(self.progressView) + self.addSubview(self.handleView) + self.progressView.layer.addSublayer(self.stripedLayer) + + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) + self.handleView.addGestureRecognizer(panGesture) + } + + // MARK: - Style + + private func style() { + Self.Style.backgroundView(self.backgroundView, model: self.model) + Self.Style.progressView(self.progressView, model: self.model) + Self.Style.stripedLayer(self.stripedLayer, model: self.model) + Self.Style.handleView(self.handleView, model: self.model) + } + + // MARK: - Layout + + private func layout() { + self.backgroundView.vertically() + self.backgroundView.trailing() + + self.backgroundViewLightLeadingConstraint = self.backgroundView.after( + self.handleView, + padding: self.model.trackSpacing + ).leading + + self.backgroundViewFilledLeadingConstraint = self.backgroundView.leading().leading + + self.backgroundViewFilledLeadingConstraint?.isActive = false + + self.progressViewConstraints = .merged { + self.progressView.leading(self.model.trackSpacing) + self.progressView.vertically() + self.progressView.width(0) + } + + self.handleConstraints = .merged { + self.handleView.after(self.progressView, padding: self.model.trackSpacing) + self.handleView.size(width: self.model.handleSize.width, height: self.model.handleSize.height) + self.handleView.centerVertically() + } + } + + // MARK: - Update + + public func update(_ oldModel: SliderVM) { + guard self.model != oldModel else { return } + + self.style() + + if self.model.shouldUpdateLayout(oldModel) { + switch self.model.style { + case .light, .striped: + self.backgroundViewFilledLeadingConstraint?.isActive = false + self.backgroundViewLightLeadingConstraint?.isActive = true + } + + self.progressViewConstraints.leading?.constant = self.model.trackSpacing + + self.invalidateIntrinsicContentSize() + self.setNeedsLayout() + } + + self.updateProgressWidthAndAppearance() + } + + private func updateProgressWidthAndAppearance() { + if self.model.style == .striped { + self.stripedLayer.frame = self.bounds + self.stripedLayer.path = self.model.stripesBezierPath(in: self.stripedLayer.bounds).cgPath + } + + let totalHorizontalPadding: CGFloat = self.model.trackSpacing + + let totalWidth = self.bounds.width - totalHorizontalPadding + let progressWidth = totalWidth * self.progress + + self.progressViewConstraints.width?.constant = max(0, progressWidth) + } + + // MARK: - Layout + + open override func layoutSubviews() { + super.layoutSubviews() + + self.backgroundView.layer.cornerRadius = + self.model.cornerRadius(for: self.backgroundView.bounds.height) + + self.progressView.layer.cornerRadius = + self.model.cornerRadius(for: self.progressView.bounds.height) + + self.handleView.layer.cornerRadius = + self.model.cornerRadius(for: self.handleView.bounds.height) + + self.updateProgressWidthAndAppearance() + + self.model.validateMinMaxValues() + } + + // MARK: - UIView + + open override func sizeThatFits(_ size: CGSize) -> CGSize { + let width = self.superview?.bounds.width ?? size.width + return CGSize( + width: min(size.width, width), + height: min(size.height, self.model.trackHeight) + ) + } + + @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { + switch gesture.state { + case .began: + self.initialProgressWidthOnDragBegan = self.progressView.frame.width + + case .changed: + let translation = gesture.translation(in: self) + + let totalWidth = self.bounds.width + let sliderWidth = max(0, totalWidth - self.model.handleSize.width - 2 * self.model.trackSpacing) + + let currentLeft = self.initialProgressWidthOnDragBegan + let newOffset = currentLeft + translation.x + let clampedOffset = min(max(newOffset, 0), sliderWidth) + + self.currentValue = self.model.steppedValue(for: clampedOffset, trackWidth: sliderWidth) + + default: + break + } + } +} + +// MARK: - Style Helpers + +extension UKSlider { + fileprivate enum Style { + static func backgroundView(_ view: UIView, model: SliderVM) { + view.backgroundColor = UIColor.red + view.layer.cornerRadius = model.cornerRadius(for: view.bounds.height) + } + + static func progressView(_ view: UIView, model: SliderVM) { + view.backgroundColor = UIColor.blue + view.layer.cornerRadius = model.cornerRadius(for: view.bounds.height) + view.layer.masksToBounds = true + } + + static func stripedLayer(_ layer: CAShapeLayer, model: SliderVM) { + layer.fillColor = model.color.main.uiColor.cgColor + switch model.style { + case .light, .striped: + layer.isHidden = true + } + } + static func handleView(_ view: UIView, model: SliderVM) { + view.backgroundColor = .black + view.layer.cornerRadius = model.cornerRadius(for: model.handleSize.height) + view.layer.masksToBounds = true + } + } +} From bdc708fc68c72ed614849f9b3504e13820d5005a Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 23 Jan 2025 17:33:46 +0100 Subject: [PATCH 2/8] improve logic for calculating slider position in UKSlider --- .../PreviewPages/SliderPreview.swift | 26 +-- .../Components/Slider/Models/SliderVM.swift | 4 +- .../Components/Slider/UKSlider.swift | 188 ++++++++++-------- 3 files changed, 109 insertions(+), 109 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift index 14310684..1ca235c5 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift @@ -2,13 +2,18 @@ import SwiftUI import ComponentsKit struct SliderPreview: View { - @State private var model = Self.initialModel - @State private var currentValue: CGFloat = Self.initialValue + @State private var model = SliderVM { + $0.style = .light + $0.minValue = 0 + $0.maxValue = 100 + $0.cornerRadius = .full + } + @State private var currentValue: CGFloat = 30 var body: some View { VStack { PreviewWrapper(title: "UIKit") { - UKSlider(model: self.model) + UKSlider(initialValue: self.currentValue, model: self.model) .preview } PreviewWrapper(title: "SwiftUI") { @@ -34,21 +39,6 @@ struct SliderPreview: View { } } } - - // MARK: - Helpers - - private static var initialValue: CGFloat { - 50 - } - - private static var initialModel: SliderVM { - var model = SliderVM() - model.style = .light - model.minValue = 0 - model.maxValue = 100 - model.cornerRadius = .full - return model - } } #Preview { diff --git a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift index 20b3baa2..a0f75845 100644 --- a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift +++ b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift @@ -161,9 +161,7 @@ extension SliderVM { } func shouldUpdateLayout(_ oldModel: Self) -> Bool { - return self.style != oldModel.style || - self.size != oldModel.size || - self.step != oldModel.step || + return self.size != oldModel.size } } diff --git a/Sources/ComponentsKit/Components/Slider/UKSlider.swift b/Sources/ComponentsKit/Components/Slider/UKSlider.swift index f9783254..6667155c 100644 --- a/Sources/ComponentsKit/Components/Slider/UKSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/UKSlider.swift @@ -13,29 +13,30 @@ open class UKSlider: UIView, UKComponent { public var currentValue: CGFloat { didSet { - self.updateProgressWidthAndAppearance() + guard self.currentValue != oldValue else { return } + + // TODO: Trigger `onValueChange` when value changes + self.updateSliderAppearance() } } - private var initialProgressWidthOnDragBegan: CGFloat = 0 - // MARK: - Subviews public let backgroundView = UIView() - public let progressView = UIView() + public let barView = UIView() public let stripedLayer = CAShapeLayer() - public let handleView = UIView() // MARK: - Layout Constraints - private var backgroundViewLightLeadingConstraint: NSLayoutConstraint? - private var backgroundViewFilledLeadingConstraint: NSLayoutConstraint? - private var progressViewConstraints: LayoutConstraints = .init() - private var handleConstraints: LayoutConstraints = .init() + private var barViewConstraints = LayoutConstraints() + private var backgroundViewConstraints = LayoutConstraints() + private var handleViewConstraints = LayoutConstraints() // MARK: - Private Properties + private var isDragging = false + private var progress: CGFloat { self.model.progress(for: self.currentValue) } @@ -49,8 +50,9 @@ open class UKSlider: UIView, UKComponent { // MARK: - Initialization public init( - initialValue: CGFloat = 50, - model: SliderVM = .init() + initialValue: CGFloat = 0, + model: SliderVM = .init(), + onValueChange: @escaping (CGFloat) -> Void = { _ in } ) { self.currentValue = initialValue self.model = model @@ -69,51 +71,20 @@ open class UKSlider: UIView, UKComponent { private func setup() { self.addSubview(self.backgroundView) - self.addSubview(self.progressView) + self.addSubview(self.barView) self.addSubview(self.handleView) - self.progressView.layer.addSublayer(self.stripedLayer) - - let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) - self.handleView.addGestureRecognizer(panGesture) + self.barView.layer.addSublayer(self.stripedLayer) } // MARK: - Style private func style() { Self.Style.backgroundView(self.backgroundView, model: self.model) - Self.Style.progressView(self.progressView, model: self.model) + Self.Style.barView(self.barView, model: self.model) Self.Style.stripedLayer(self.stripedLayer, model: self.model) Self.Style.handleView(self.handleView, model: self.model) } - // MARK: - Layout - - private func layout() { - self.backgroundView.vertically() - self.backgroundView.trailing() - - self.backgroundViewLightLeadingConstraint = self.backgroundView.after( - self.handleView, - padding: self.model.trackSpacing - ).leading - - self.backgroundViewFilledLeadingConstraint = self.backgroundView.leading().leading - - self.backgroundViewFilledLeadingConstraint?.isActive = false - - self.progressViewConstraints = .merged { - self.progressView.leading(self.model.trackSpacing) - self.progressView.vertically() - self.progressView.width(0) - } - - self.handleConstraints = .merged { - self.handleView.after(self.progressView, padding: self.model.trackSpacing) - self.handleView.size(width: self.model.handleSize.width, height: self.model.handleSize.height) - self.handleView.centerVertically() - } - } - // MARK: - Update public func update(_ oldModel: SliderVM) { @@ -122,84 +93,124 @@ open class UKSlider: UIView, UKComponent { self.style() if self.model.shouldUpdateLayout(oldModel) { - switch self.model.style { - case .light, .striped: - self.backgroundViewFilledLeadingConstraint?.isActive = false - self.backgroundViewLightLeadingConstraint?.isActive = true - } - - self.progressViewConstraints.leading?.constant = self.model.trackSpacing + self.barViewConstraints.height?.constant = self.model.trackHeight + self.backgroundViewConstraints.height?.constant = self.model.trackHeight + self.handleViewConstraints.height?.constant = self.model.handleSize.height + self.handleViewConstraints.width?.constant = self.model.handleSize.width - self.invalidateIntrinsicContentSize() - self.setNeedsLayout() + UIView.performWithoutAnimation { + self.layoutIfNeeded() + } } - self.updateProgressWidthAndAppearance() + self.updateSliderAppearance() } - private func updateProgressWidthAndAppearance() { + private func updateSliderAppearance() { if self.model.style == .striped { self.stripedLayer.frame = self.bounds self.stripedLayer.path = self.model.stripesBezierPath(in: self.stripedLayer.bounds).cgPath } - let totalHorizontalPadding: CGFloat = self.model.trackSpacing - - let totalWidth = self.bounds.width - totalHorizontalPadding - let progressWidth = totalWidth * self.progress - - self.progressViewConstraints.width?.constant = max(0, progressWidth) + let barWidth = self.model.barWidth(for: self.bounds.width, progress: self.progress) + self.barViewConstraints.width?.constant = barWidth } // MARK: - Layout + private func layout() { + self.barViewConstraints = .merged { + self.barView.leading() + self.barView.centerVertically() + self.barView.height(self.model.trackHeight) + self.barView.width(0) + } + + self.backgroundViewConstraints = .merged { + self.backgroundView.trailing() + self.backgroundView.centerVertically() + self.backgroundView.height(self.model.trackHeight) + } + + self.handleViewConstraints = .merged { + self.handleView.after(self.barView, padding: self.model.trackSpacing) + self.handleView.before(self.backgroundView, padding: self.model.trackSpacing) + self.handleView.size( + width: self.model.handleSize.width, + height: self.model.handleSize.height + ) + self.handleView.centerVertically() + } + } + open override func layoutSubviews() { super.layoutSubviews() self.backgroundView.layer.cornerRadius = self.model.cornerRadius(for: self.backgroundView.bounds.height) - self.progressView.layer.cornerRadius = - self.model.cornerRadius(for: self.progressView.bounds.height) + self.barView.layer.cornerRadius = + self.model.cornerRadius(for: self.barView.bounds.height) + // TODO: Calculate corner radius in SwiftUI component according to handler's width self.handleView.layer.cornerRadius = - self.model.cornerRadius(for: self.handleView.bounds.height) + self.model.cornerRadius(for: self.handleView.bounds.width) - self.updateProgressWidthAndAppearance() + self.updateSliderAppearance() self.model.validateMinMaxValues() } - // MARK: - UIView + // MARK: - UIView Methods open override func sizeThatFits(_ size: CGSize) -> CGSize { let width = self.superview?.bounds.width ?? size.width return CGSize( width: min(size.width, width), - height: min(size.height, self.model.trackHeight) + height: min(size.height, self.model.handleSize.height) ) } - @objc private func handlePan(_ gesture: UIPanGestureRecognizer) { - switch gesture.state { - case .began: - self.initialProgressWidthOnDragBegan = self.progressView.frame.width + open override func touchesBegan( + _ touches: Set, + with event: UIEvent? + ) { + guard let point = touches.first?.location(in: self), + self.hitTest(point, with: nil) == self.handleView + else { return } - case .changed: - let translation = gesture.translation(in: self) + self.isDragging = true + } - let totalWidth = self.bounds.width - let sliderWidth = max(0, totalWidth - self.model.handleSize.width - 2 * self.model.trackSpacing) + open override func touchesMoved( + _ touches: Set, + with event: UIEvent? + ) { + guard self.isDragging, + let translation = touches.first?.location(in: self) + else { return } - let currentLeft = self.initialProgressWidthOnDragBegan - let newOffset = currentLeft + translation.x - let clampedOffset = min(max(newOffset, 0), sliderWidth) + let totalWidth = self.bounds.width + let sliderWidth = max(0, totalWidth - self.model.handleSize.width - 2 * self.model.trackSpacing) - self.currentValue = self.model.steppedValue(for: clampedOffset, trackWidth: sliderWidth) + let newOffset = translation.x - self.model.trackSpacing - self.model.handleSize.width / 2 + let clampedOffset = min(max(newOffset, 0), sliderWidth) - default: - break - } + self.currentValue = self.model.steppedValue(for: clampedOffset, trackWidth: sliderWidth) + } + + open override func touchesEnded( + _ touches: Set, + with event: UIEvent? + ) { + self.isDragging = false + } + + open override func touchesCancelled( + _ touches: Set, + with event: UIEvent? + ) { + self.isDragging = false } } @@ -208,12 +219,12 @@ open class UKSlider: UIView, UKComponent { extension UKSlider { fileprivate enum Style { static func backgroundView(_ view: UIView, model: SliderVM) { - view.backgroundColor = UIColor.red + view.backgroundColor = model.color.background.uiColor view.layer.cornerRadius = model.cornerRadius(for: view.bounds.height) } - static func progressView(_ view: UIView, model: SliderVM) { - view.backgroundColor = UIColor.blue + static func barView(_ view: UIView, model: SliderVM) { + view.backgroundColor = model.color.main.uiColor view.layer.cornerRadius = model.cornerRadius(for: view.bounds.height) view.layer.masksToBounds = true } @@ -225,9 +236,10 @@ extension UKSlider { layer.isHidden = true } } + static func handleView(_ view: UIView, model: SliderVM) { - view.backgroundColor = .black - view.layer.cornerRadius = model.cornerRadius(for: model.handleSize.height) + view.backgroundColor = model.color.main.uiColor + view.layer.cornerRadius = model.cornerRadius(for: model.handleSize.width) view.layer.masksToBounds = true } } From e986110255abf570dc70ba32fb94fb917f54a2fa Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Fri, 24 Jan 2025 18:29:30 +0300 Subject: [PATCH 3/8] added onValueChange --- Sources/ComponentsKit/Components/Slider/UKSlider.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/Slider/UKSlider.swift b/Sources/ComponentsKit/Components/Slider/UKSlider.swift index 6667155c..b50f83a5 100644 --- a/Sources/ComponentsKit/Components/Slider/UKSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/UKSlider.swift @@ -5,6 +5,8 @@ import UIKit open class UKSlider: UIView, UKComponent { // MARK: - Properties + public var onValueChange: (CGFloat) -> Void + public var model: SliderVM { didSet { self.update(oldValue) @@ -15,8 +17,9 @@ open class UKSlider: UIView, UKComponent { didSet { guard self.currentValue != oldValue else { return } - // TODO: Trigger `onValueChange` when value changes self.updateSliderAppearance() + + self.onValueChange(self.currentValue) } } @@ -56,6 +59,7 @@ open class UKSlider: UIView, UKComponent { ) { self.currentValue = initialValue self.model = model + self.onValueChange = onValueChange super.init(frame: .zero) self.setup() From d2e55927711cc60b241b2767e8c45c1978da6557 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Fri, 24 Jan 2025 19:48:02 +0300 Subject: [PATCH 4/8] handleOverlayView --- .../Components/Slider/UKSlider.swift | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Sources/ComponentsKit/Components/Slider/UKSlider.swift b/Sources/ComponentsKit/Components/Slider/UKSlider.swift index b50f83a5..c792a07a 100644 --- a/Sources/ComponentsKit/Components/Slider/UKSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/UKSlider.swift @@ -29,12 +29,14 @@ open class UKSlider: UIView, UKComponent { public let barView = UIView() public let stripedLayer = CAShapeLayer() public let handleView = UIView() + private let handleOverlayView = UIView() // MARK: - Layout Constraints private var barViewConstraints = LayoutConstraints() private var backgroundViewConstraints = LayoutConstraints() private var handleViewConstraints = LayoutConstraints() + private var handleOverlayViewConstraints = LayoutConstraints() // MARK: - Private Properties @@ -78,6 +80,7 @@ open class UKSlider: UIView, UKComponent { self.addSubview(self.barView) self.addSubview(self.handleView) self.barView.layer.addSublayer(self.stripedLayer) + self.handleView.addSubview(self.handleOverlayView) } // MARK: - Style @@ -87,6 +90,7 @@ open class UKSlider: UIView, UKComponent { Self.Style.barView(self.barView, model: self.model) Self.Style.stripedLayer(self.stripedLayer, model: self.model) Self.Style.handleView(self.handleView, model: self.model) + Self.Style.handleOverlayView(self.handleOverlayView, model: self.model) } // MARK: - Update @@ -102,6 +106,9 @@ open class UKSlider: UIView, UKComponent { self.handleViewConstraints.height?.constant = self.model.handleSize.height self.handleViewConstraints.width?.constant = self.model.handleSize.width + self.handleOverlayViewConstraints.height?.constant = self.model.handleOverlaySide + self.handleOverlayViewConstraints.width?.constant = self.model.handleOverlaySide + UIView.performWithoutAnimation { self.layoutIfNeeded() } @@ -145,6 +152,14 @@ open class UKSlider: UIView, UKComponent { ) self.handleView.centerVertically() } + + self.handleOverlayViewConstraints = .merged { + self.handleOverlayView.center() + self.handleOverlayView.size( + width: self.model.handleOverlaySide, + height: self.model.handleOverlaySide + ) + } } open override func layoutSubviews() { @@ -156,12 +171,10 @@ open class UKSlider: UIView, UKComponent { self.barView.layer.cornerRadius = self.model.cornerRadius(for: self.barView.bounds.height) - // TODO: Calculate corner radius in SwiftUI component according to handler's width self.handleView.layer.cornerRadius = self.model.cornerRadius(for: self.handleView.bounds.width) self.updateSliderAppearance() - self.model.validateMinMaxValues() } @@ -246,5 +259,15 @@ extension UKSlider { view.layer.cornerRadius = model.cornerRadius(for: model.handleSize.width) view.layer.masksToBounds = true } + + static func handleOverlayView(_ view: UIView, model: SliderVM) { + if model.size == .large { + view.isHidden = false + view.backgroundColor = model.color.contrast.uiColor + view.layer.cornerRadius = model.cornerRadius(for: model.handleOverlaySide) + } else { + view.isHidden = true + } + } } } From 2812bd241a95f79e5d2ba147f4f58025cbd49032 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Fri, 24 Jan 2025 19:49:16 +0300 Subject: [PATCH 5/8] added stripedLayer --- .../ComponentsKit/Components/Slider/UKSlider.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/ComponentsKit/Components/Slider/UKSlider.swift b/Sources/ComponentsKit/Components/Slider/UKSlider.swift index c792a07a..92acedc7 100644 --- a/Sources/ComponentsKit/Components/Slider/UKSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/UKSlider.swift @@ -79,7 +79,7 @@ open class UKSlider: UIView, UKComponent { self.addSubview(self.backgroundView) self.addSubview(self.barView) self.addSubview(self.handleView) - self.barView.layer.addSublayer(self.stripedLayer) + self.backgroundView.layer.addSublayer(self.stripedLayer) self.handleView.addSubview(self.handleOverlayView) } @@ -119,7 +119,7 @@ open class UKSlider: UIView, UKComponent { private func updateSliderAppearance() { if self.model.style == .striped { - self.stripedLayer.frame = self.bounds + self.stripedLayer.frame = self.backgroundView.bounds self.stripedLayer.path = self.model.stripesBezierPath(in: self.stripedLayer.bounds).cgPath } @@ -237,7 +237,11 @@ extension UKSlider { fileprivate enum Style { static func backgroundView(_ view: UIView, model: SliderVM) { view.backgroundColor = model.color.background.uiColor + if model.style == .striped { + view.backgroundColor = .clear + } view.layer.cornerRadius = model.cornerRadius(for: view.bounds.height) + view.layer.masksToBounds = true } static func barView(_ view: UIView, model: SliderVM) { @@ -249,8 +253,10 @@ extension UKSlider { static func stripedLayer(_ layer: CAShapeLayer, model: SliderVM) { layer.fillColor = model.color.main.uiColor.cgColor switch model.style { - case .light, .striped: + case .light: layer.isHidden = true + case .striped: + layer.isHidden = false } } From 9ae7ed5754f226299e141ff857db46e3c47aeebb Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Fri, 24 Jan 2025 19:50:11 +0300 Subject: [PATCH 6/8] fix swiftui handle cornerRadius --- Sources/ComponentsKit/Components/Slider/SUSlider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ComponentsKit/Components/Slider/SUSlider.swift b/Sources/ComponentsKit/Components/Slider/SUSlider.swift index 76f3b262..257a1c35 100644 --- a/Sources/ComponentsKit/Components/Slider/SUSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/SUSlider.swift @@ -42,7 +42,7 @@ public struct SUSlider: View { .frame(width: barWidth, height: self.model.trackHeight) // Handle - RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.handleSize.height)) + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.handleSize.width)) .foregroundStyle(self.model.color.main.color) .frame(width: self.model.handleSize.width, height: self.model.handleSize.height) .overlay( From 9701c2c25d6b5625751c3896ffe14ad19886515b Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Fri, 24 Jan 2025 19:57:42 +0300 Subject: [PATCH 7/8] Documentation --- .../Components/Slider/UKSlider.swift | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/Sources/ComponentsKit/Components/Slider/UKSlider.swift b/Sources/ComponentsKit/Components/Slider/UKSlider.swift index 92acedc7..48bffbcf 100644 --- a/Sources/ComponentsKit/Components/Slider/UKSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/UKSlider.swift @@ -5,30 +5,40 @@ import UIKit open class UKSlider: UIView, UKComponent { // MARK: - Properties + /// A closure that is triggered when the `currentValue` changes. public var onValueChange: (CGFloat) -> Void + /// A model that defines the appearance properties. public var model: SliderVM { didSet { self.update(oldValue) } } + /// The current value of the slider. public var currentValue: CGFloat { didSet { guard self.currentValue != oldValue else { return } - self.updateSliderAppearance() - self.onValueChange(self.currentValue) } } // MARK: - Subviews + /// The background view of the slider track. public let backgroundView = UIView() + + /// The filled portion of the slider track. public let barView = UIView() + + /// A shape layer used to render striped styling. public let stripedLayer = CAShapeLayer() + + /// The draggable handle representing the current value. public let handleView = UIView() + + /// An overlay view for handle for the `large` style. private let handleOverlayView = UIView() // MARK: - Layout Constraints @@ -46,7 +56,7 @@ open class UKSlider: UIView, UKComponent { self.model.progress(for: self.currentValue) } - // MARK: - UIView + // MARK: - UIView Properties open override var intrinsicContentSize: CGSize { return self.sizeThatFits(UIView.layoutFittingExpandedSize) @@ -54,6 +64,11 @@ open class UKSlider: UIView, UKComponent { // MARK: - Initialization + /// Initializer. + /// - Parameters: + /// - initialValue: The initial slider value. Defaults to `0`. + /// - model: A model that defines the appearance properties. + /// - onValueChange: A closure triggered whenever `currentValue` changes. public init( initialValue: CGFloat = 0, model: SliderVM = .init(), From 58e1ad59919e87578109082fc8bcb5213615b87b Mon Sep 17 00:00:00 2001 From: Mikhail Date: Mon, 27 Jan 2025 11:26:46 +0100 Subject: [PATCH 8/8] improve `handleOverlay` styling and layout --- .../Components/Slider/Models/SliderVM.swift | 9 ++++++ .../Components/Slider/UKSlider.swift | 29 +++++++------------ 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift index a0f75845..beb271b9 100644 --- a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift +++ b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift @@ -156,6 +156,15 @@ extension SliderVM { // MARK: - UIKit Helpers extension SliderVM { + var isHandleOverlayVisible: Bool { + switch self.size { + case .small, .medium: + return false + case .large: + return true + } + } + func stripesBezierPath(in rect: CGRect) -> UIBezierPath { return UIBezierPath(cgPath: self.stripesCGPath(in: rect)) } diff --git a/Sources/ComponentsKit/Components/Slider/UKSlider.swift b/Sources/ComponentsKit/Components/Slider/UKSlider.swift index 48bffbcf..07ce9b96 100644 --- a/Sources/ComponentsKit/Components/Slider/UKSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/UKSlider.swift @@ -46,7 +46,6 @@ open class UKSlider: UIView, UKComponent { private var barViewConstraints = LayoutConstraints() private var backgroundViewConstraints = LayoutConstraints() private var handleViewConstraints = LayoutConstraints() - private var handleOverlayViewConstraints = LayoutConstraints() // MARK: - Private Properties @@ -121,9 +120,6 @@ open class UKSlider: UIView, UKComponent { self.handleViewConstraints.height?.constant = self.model.handleSize.height self.handleViewConstraints.width?.constant = self.model.handleSize.width - self.handleOverlayViewConstraints.height?.constant = self.model.handleOverlaySide - self.handleOverlayViewConstraints.width?.constant = self.model.handleOverlaySide - UIView.performWithoutAnimation { self.layoutIfNeeded() } @@ -168,13 +164,11 @@ open class UKSlider: UIView, UKComponent { self.handleView.centerVertically() } - self.handleOverlayViewConstraints = .merged { - self.handleOverlayView.center() - self.handleOverlayView.size( - width: self.model.handleOverlaySide, - height: self.model.handleOverlaySide - ) - } + self.handleOverlayView.center() + self.handleOverlayView.size( + width: self.model.handleOverlaySide, + height: self.model.handleOverlaySide + ) } open override func layoutSubviews() { @@ -189,6 +183,9 @@ open class UKSlider: UIView, UKComponent { self.handleView.layer.cornerRadius = self.model.cornerRadius(for: self.handleView.bounds.width) + self.handleOverlayView.layer.cornerRadius = + self.model.cornerRadius(for: self.handleOverlayView.bounds.width) + self.updateSliderAppearance() self.model.validateMinMaxValues() } @@ -282,13 +279,9 @@ extension UKSlider { } static func handleOverlayView(_ view: UIView, model: SliderVM) { - if model.size == .large { - view.isHidden = false - view.backgroundColor = model.color.contrast.uiColor - view.layer.cornerRadius = model.cornerRadius(for: model.handleOverlaySide) - } else { - view.isHidden = true - } + view.isVisible = model.isHandleOverlayVisible + view.backgroundColor = model.color.contrast.uiColor + view.layer.cornerRadius = model.cornerRadius(for: model.handleOverlaySide) } } }