From 41bb055d363970dfc96b2064382c7bf72da6ec12 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Sat, 18 Jan 2025 00:14:56 +0300 Subject: [PATCH 1/8] SUSlider - SUSlider - SliderVM - SliderPreview into App - Documentation --- .../PreviewPages/SliderPreview.swift | 48 +++++ Examples/DemosApp/DemosApp/Core/App.swift | 3 + .../Slider/Models/SliderStyle.swift | 9 + .../Components/Slider/Models/SliderVM.swift | 176 ++++++++++++++++++ .../Components/Slider/SUSlider.swift | 128 +++++++++++++ 5 files changed, 364 insertions(+) create mode 100644 Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift create mode 100644 Sources/ComponentsKit/Components/Slider/Models/SliderStyle.swift create mode 100644 Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift create mode 100644 Sources/ComponentsKit/Components/Slider/SUSlider.swift diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift new file mode 100644 index 00000000..11d96447 --- /dev/null +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift @@ -0,0 +1,48 @@ +import SwiftUI +import ComponentsKit + +struct SliderPreview: View { + @State private var model = Self.initialModel + @State private var currentValue: CGFloat = Self.initialValue + + var body: some View { + VStack { + PreviewWrapper(title: "SwiftUI") { + SUSlider(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("Step", selection: self.$model.step) { + Text("1").tag(CGFloat(1)) + Text("5").tag(CGFloat(5)) + Text("10").tag(CGFloat(10)) + Text("25").tag(CGFloat(25)) + Text("50").tag(CGFloat(50)) + } + Picker("Style", selection: self.$model.style) { + Text("Light").tag(SliderVM.Style.light) + Text("Striped").tag(SliderVM.Style.striped) + } + } + } + } + + // 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 + } +} diff --git a/Examples/DemosApp/DemosApp/Core/App.swift b/Examples/DemosApp/DemosApp/Core/App.swift index ab7aaf9e..01d9e8c9 100644 --- a/Examples/DemosApp/DemosApp/Core/App.swift +++ b/Examples/DemosApp/DemosApp/Core/App.swift @@ -47,6 +47,9 @@ struct App: View { NavigationLinkWithTitle("Segmented Control") { SegmentedControlPreview() } + NavigationLinkWithTitle("Slider") { + SliderPreview() + } NavigationLinkWithTitle("Text Input") { TextInputPreviewPreview() } diff --git a/Sources/ComponentsKit/Components/Slider/Models/SliderStyle.swift b/Sources/ComponentsKit/Components/Slider/Models/SliderStyle.swift new file mode 100644 index 00000000..c4a70868 --- /dev/null +++ b/Sources/ComponentsKit/Components/Slider/Models/SliderStyle.swift @@ -0,0 +1,9 @@ +import Foundation + +extension SliderVM { + /// Defines the visual styles for the slider component. + public enum Style { + case light + case striped + } +} diff --git a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift new file mode 100644 index 00000000..a95a7244 --- /dev/null +++ b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift @@ -0,0 +1,176 @@ +import SwiftUI + +/// A model that defines the appearance properties for a slider component. +public struct SliderVM: ComponentVM { + /// The color of the slider. + /// + /// Defaults to `.accent`. + public var color: ComponentColor = .accent + + /// The visual style of the slider component. + /// + /// Defaults to `.light`. + public var style: Style = .light + + /// The size of the slider. + /// + /// Defaults to `.medium`. + public var size: ComponentSize = .medium + + /// The minimum value of the slider. + public var minValue: CGFloat = 0 + + /// The maximum value of the slider. + public var maxValue: CGFloat = 100 + + /// The corner radius of the slider track. + /// + /// Defaults to `.full`. + public var cornerRadius: ComponentRadius = .full + + /// The step value for the slider. + /// + /// Defaults to `1`. + public var step: CGFloat = 1 + + /// Initializes a new instance of `SliderVM` with default values. + public init() {} +} + +// MARK: - Shared Helpers + +extension SliderVM { + var backgroundHeight: CGFloat { + switch self.style { + case .light: + switch self.size { + case .small: + return 6 + case .medium: + return 12 + case .large: + return 32 + } + case .striped: + switch self.size { + case .small: + return 6 + case .medium: + return 12 + case .large: + return 32 + } + } + } + var handleSize: CGSize { + switch self.size { + case .small, .medium: + return CGSize(width: 20, height: 32) + case .large: + return CGSize(width: 40, height: 40) + } + } + func cornerRadius(for height: CGFloat) -> CGFloat { + switch self.cornerRadius { + case .none: + return 0 + case .small: + return height / 3.5 + case .medium: + return height / 3.0 + case .large: + return height / 2.5 + case .full: + return height / 2.0 + case .custom(let value): + return min(value, height / 2) + } + } + var barSpacing: CGFloat { + return 4 + } + var handleOverlaySize: CGFloat { + 12 + } + var backgroundColor: UniversalColor { + switch style { + case .light: + return self.color.background + case .striped: + return self.color.main + } + } + var barColor: UniversalColor { + switch style { + case .light: + return self.color.main + case .striped: + return self.color.contrast + } + } + 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 + } +} + +extension SliderVM { + func progress(for currentValue: CGFloat) -> CGFloat { + let range = self.maxValue - self.minValue + guard range > 0 else { return 0 } + let normalized = (currentValue - self.minValue) / range + return max(0, min(1, normalized)) + } +} + +// MARK: - UIKit Helpers + +extension SliderVM { + func stripesBezierPath(in rect: CGRect) -> UIBezierPath { + return UIBezierPath(cgPath: self.stripesCGPath(in: rect)) + } + + func shouldUpdateLayout(_ oldModel: Self) -> Bool { + return self.style != oldModel.style || + self.size != oldModel.size || + self.step != oldModel.step + } +} + +// MARK: - SwiftUI Helpers + +extension SliderVM { + func stripesPath(in rect: CGRect) -> Path { + Path(self.stripesCGPath(in: rect)) + } +} + +// MARK: - Validation + +extension SliderVM { + func validateMinMaxValues() { + if self.minValue > self.maxValue { + assertionFailure("Min value must be less than max value") + } + } +} diff --git a/Sources/ComponentsKit/Components/Slider/SUSlider.swift b/Sources/ComponentsKit/Components/Slider/SUSlider.swift new file mode 100644 index 00000000..5fa137e4 --- /dev/null +++ b/Sources/ComponentsKit/Components/Slider/SUSlider.swift @@ -0,0 +1,128 @@ +import SwiftUI + +/// A SwiftUI component that displays a slider. +public struct SUSlider: View { + // MARK: - Properties + + /// A model that defines the appearance properties. + public var model: SliderVM + + /// A binding to control the current value. + @Binding public var currentValue: CGFloat + + private var progress: CGFloat { + self.model.progress(for: self.currentValue) + } + + // MARK: - Initializer + + /// Initializer. + /// - Parameters: + /// - currentValue: A binding to the current value. + /// - model: A model that defines the appearance properties. + public init( + currentValue: Binding, + model: SliderVM = .init() + ) { + self._currentValue = currentValue + self.model = model + } + + // MARK: - Body + + public var body: some View { + GeometryReader { geometry in + let handleWidth = self.model.handleSize.width + let handleHeight = self.model.handleSize.height + + let sliderHeight = self.model.backgroundHeight + + let containerHeight = max(sliderHeight, handleHeight) + + // Calculate the width available for the track, excluding handle width + spacing + let sliderWidth = max(0, geometry.size.width - handleWidth - (2 * self.model.barSpacing)) + + // The track width based on the progress + let leftWidth = progress * sliderWidth + let rightWidth = sliderWidth - leftWidth + + ZStack(alignment: .center) { + HStack(spacing: self.model.barSpacing) { + // Progress segment + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: sliderHeight)) + .foregroundStyle(self.model.color.main.color) + .frame(width: max(leftWidth, 0)) + + // Handle + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: handleHeight)) + .foregroundStyle(self.model.color.main.color) + .frame(width: handleWidth, height: handleHeight) + .overlay( + Group { + if self.model.size == .large { + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.handleOverlaySize)) + .foregroundStyle(self.model.color.contrast.color) + .frame(width: self.model.handleOverlaySize, height: self.model.handleOverlaySize) + } + } + ) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { value in + let newOffset = leftWidth + value.translation.width + let fixedOffset = min(max(newOffset, 0), sliderWidth) + let newProgress = sliderWidth > 0 ? (fixedOffset / sliderWidth) : 0 + + let newValue = self.model.minValue + newProgress * (self.model.maxValue - self.model.minValue) + + let steppedValue: CGFloat + if self.model.step > 0 { + let stepsCount = (newValue / self.model.step).rounded() + steppedValue = stepsCount * self.model.step + } else { + steppedValue = newValue + } + self.currentValue = steppedValue + } + ) + + // Remaining segment + switch self.model.style { + case .light: + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: sliderHeight)) + .foregroundStyle(self.model.backgroundColor.color) + .frame(width: max(rightWidth, 0)) + + case .striped: + ZStack { + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: sliderHeight)) + .foregroundStyle(self.model.color.contrast.color) + + StripesShapeSlider(model: self.model) + .foregroundStyle(self.model.color.main.color) + .cornerRadius(self.model.cornerRadius(for: sliderHeight)) + } + .frame(width: max(rightWidth, 0)) + } + } + .frame(height: sliderHeight) + } + .frame(width: geometry.size.width, height: containerHeight) + // Center the slider vertically within the available space + .frame(maxHeight: .infinity, alignment: .center) + } + .onAppear { + self.model.validateMinMaxValues() + } + } +} + +// MARK: - Helpers + +struct StripesShapeSlider: Shape { + var model: SliderVM + + func path(in rect: CGRect) -> Path { + self.model.stripesPath(in: rect) + } +} From 50b04b50ce2384ce77eda447cf45c4f805027ed5 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 20 Jan 2025 11:01:57 +0300 Subject: [PATCH 2/8] steppedValue calculation extracted into model --- .../Components/Slider/Models/SliderVM.swift | 17 +++++++++++++++++ .../Components/Slider/SUSlider.swift | 16 +++------------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift index a95a7244..cde211d0 100644 --- a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift +++ b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift @@ -134,6 +134,23 @@ extension SliderVM { } } +extension SliderVM { + func steppedValue(for offset: CGFloat, trackWidth: CGFloat) -> CGFloat { + guard trackWidth > 0 else { return self.minValue } + + let newProgress = offset / trackWidth + + let newValue = self.minValue + newProgress * (self.maxValue - self.minValue) + + if self.step > 0 { + let stepsCount = (newValue / self.step).rounded() + return stepsCount * self.step + } else { + return newValue + } + } +} + extension SliderVM { func progress(for currentValue: CGFloat) -> CGFloat { let range = self.maxValue - self.minValue diff --git a/Sources/ComponentsKit/Components/Slider/SUSlider.swift b/Sources/ComponentsKit/Components/Slider/SUSlider.swift index 5fa137e4..f99f7238 100644 --- a/Sources/ComponentsKit/Components/Slider/SUSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/SUSlider.swift @@ -70,19 +70,9 @@ public struct SUSlider: View { DragGesture(minimumDistance: 0) .onChanged { value in let newOffset = leftWidth + value.translation.width - let fixedOffset = min(max(newOffset, 0), sliderWidth) - let newProgress = sliderWidth > 0 ? (fixedOffset / sliderWidth) : 0 - - let newValue = self.model.minValue + newProgress * (self.model.maxValue - self.model.minValue) - - let steppedValue: CGFloat - if self.model.step > 0 { - let stepsCount = (newValue / self.model.step).rounded() - steppedValue = stepsCount * self.model.step - } else { - steppedValue = newValue - } - self.currentValue = steppedValue + let clampedOffset = min(max(newOffset, 0), sliderWidth) + + self.currentValue = self.model.steppedValue(for: clampedOffset, trackWidth: sliderWidth) } ) From d82022321bf1f4681c17bc4f4cf9e4c0d717b8c1 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 20 Jan 2025 15:33:23 +0300 Subject: [PATCH 3/8] small fix and documentation --- .../ComponentsPreview/PreviewPages/SliderPreview.swift | 4 ++++ Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift index 11d96447..99c4df99 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/SliderPreview.swift @@ -46,3 +46,7 @@ struct SliderPreview: View { return model } } + +#Preview { + SliderPreview() +} diff --git a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift index cde211d0..518bc614 100644 --- a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift +++ b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift @@ -23,7 +23,7 @@ public struct SliderVM: ComponentVM { /// The maximum value of the slider. public var maxValue: CGFloat = 100 - /// The corner radius of the slider track. + /// The corner radius of the slider track and handle. /// /// Defaults to `.full`. public var cornerRadius: ComponentRadius = .full From 0f5e8757828f545c5058267930f1ddc03cff08c0 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 20 Jan 2025 15:44:13 +0300 Subject: [PATCH 4/8] suppressing the warning - added "@unchecked Sendable" for StripesShapeSlider --- .../Components/Slider/Models/SliderVM.swift | 34 ++++++------------- .../Components/Slider/SUSlider.swift | 10 +++--- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift index 518bc614..4431fd84 100644 --- a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift +++ b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift @@ -40,26 +40,14 @@ public struct SliderVM: ComponentVM { // MARK: - Shared Helpers extension SliderVM { - var backgroundHeight: CGFloat { - switch self.style { - case .light: - switch self.size { - case .small: - return 6 - case .medium: - return 12 - case .large: - return 32 - } - case .striped: - switch self.size { - case .small: - return 6 - case .medium: - return 12 - case .large: - return 32 - } + var trackHeight: CGFloat { + switch self.size { + case .small: + return 6 + case .medium: + return 12 + case .large: + return 32 } } var handleSize: CGSize { @@ -86,10 +74,10 @@ extension SliderVM { return min(value, height / 2) } } - var barSpacing: CGFloat { + var trackSpacing: CGFloat { return 4 } - var handleOverlaySize: CGFloat { + var handleOverlaySide: CGFloat { 12 } var backgroundColor: UniversalColor { @@ -118,7 +106,7 @@ extension SliderVM { let radians = stripeAngle.radians let dx = rect.height * tan(radians) - for x in stride(from: dx, through: rect.width + rect.height, by: step) { + for x in stride(from: rect.width + rect.height, through: dx, 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) diff --git a/Sources/ComponentsKit/Components/Slider/SUSlider.swift b/Sources/ComponentsKit/Components/Slider/SUSlider.swift index f99f7238..15f75ce1 100644 --- a/Sources/ComponentsKit/Components/Slider/SUSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/SUSlider.swift @@ -35,19 +35,19 @@ public struct SUSlider: View { let handleWidth = self.model.handleSize.width let handleHeight = self.model.handleSize.height - let sliderHeight = self.model.backgroundHeight + let sliderHeight = self.model.trackHeight let containerHeight = max(sliderHeight, handleHeight) // Calculate the width available for the track, excluding handle width + spacing - let sliderWidth = max(0, geometry.size.width - handleWidth - (2 * self.model.barSpacing)) + let sliderWidth = max(0, geometry.size.width - handleWidth - (2 * self.model.trackSpacing)) // The track width based on the progress let leftWidth = progress * sliderWidth let rightWidth = sliderWidth - leftWidth ZStack(alignment: .center) { - HStack(spacing: self.model.barSpacing) { + HStack(spacing: self.model.trackSpacing) { // Progress segment RoundedRectangle(cornerRadius: self.model.cornerRadius(for: sliderHeight)) .foregroundStyle(self.model.color.main.color) @@ -60,9 +60,9 @@ public struct SUSlider: View { .overlay( Group { if self.model.size == .large { - RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.handleOverlaySize)) + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.handleOverlaySide)) .foregroundStyle(self.model.color.contrast.color) - .frame(width: self.model.handleOverlaySize, height: self.model.handleOverlaySize) + .frame(width: self.model.handleOverlaySide, height: self.model.handleOverlaySide) } } ) From 2a2e01106cd2713989b6ea03bd2574f2a1687e3b Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 20 Jan 2025 15:44:37 +0300 Subject: [PATCH 5/8] suppressing the warning - added "@unchecked Sendable" for StripesShapeSlider --- 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 15f75ce1..d0104e1b 100644 --- a/Sources/ComponentsKit/Components/Slider/SUSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/SUSlider.swift @@ -109,7 +109,7 @@ public struct SUSlider: View { // MARK: - Helpers -struct StripesShapeSlider: Shape { +struct StripesShapeSlider: Shape, @unchecked Sendable { var model: SliderVM func path(in rect: CGRect) -> Path { From 8f3e2a3aacc3cfd2718270fa5b8f2c275b50cae0 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Mon, 20 Jan 2025 20:22:33 +0300 Subject: [PATCH 6/8] added layout properties into model --- .../Components/Slider/Models/SliderVM.swift | 28 ++++++ .../Components/Slider/SUSlider.swift | 93 +++++++++---------- 2 files changed, 71 insertions(+), 50 deletions(-) diff --git a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift index 4431fd84..944f1a78 100644 --- a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift +++ b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift @@ -148,6 +148,34 @@ extension SliderVM { } } +public extension SliderVM { + var sliderHeight: CGFloat { + self.trackHeight + } + + var containerHeight: CGFloat { + max(self.handleSize.height, self.trackHeight) + } + + func leftWidth(for totalWidth: CGFloat, progress: CGFloat) -> CGFloat { + let sliderWidth = max(0, totalWidth - self.handleSize.width - 2 * self.trackSpacing) + return sliderWidth * progress + } + + func rightWidth(for totalWidth: CGFloat, progress: CGFloat) -> CGFloat { + let sliderWidth = max(0, totalWidth - self.handleSize.width - 2 * self.trackSpacing) + return sliderWidth - self.leftWidth(for: totalWidth, progress: progress) + } + + func barWidth(for totalWidth: CGFloat, progress: CGFloat) -> CGFloat { + self.leftWidth(for: totalWidth, progress: progress) + } + + func backgroundWidth(for totalWidth: CGFloat, progress: CGFloat) -> CGFloat { + self.rightWidth(for: totalWidth, progress: progress) + } +} + // MARK: - UIKit Helpers extension SliderVM { diff --git a/Sources/ComponentsKit/Components/Slider/SUSlider.swift b/Sources/ComponentsKit/Components/Slider/SUSlider.swift index d0104e1b..86b98f6b 100644 --- a/Sources/ComponentsKit/Components/Slider/SUSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/SUSlider.swift @@ -32,56 +32,52 @@ public struct SUSlider: View { public var body: some View { GeometryReader { geometry in - let handleWidth = self.model.handleSize.width - let handleHeight = self.model.handleSize.height - - let sliderHeight = self.model.trackHeight - - let containerHeight = max(sliderHeight, handleHeight) - - // Calculate the width available for the track, excluding handle width + spacing - let sliderWidth = max(0, geometry.size.width - handleWidth - (2 * self.model.trackSpacing)) - - // The track width based on the progress - let leftWidth = progress * sliderWidth - let rightWidth = sliderWidth - leftWidth - - ZStack(alignment: .center) { - HStack(spacing: self.model.trackSpacing) { - // Progress segment - RoundedRectangle(cornerRadius: self.model.cornerRadius(for: sliderHeight)) - .foregroundStyle(self.model.color.main.color) - .frame(width: max(leftWidth, 0)) - - // Handle - RoundedRectangle(cornerRadius: self.model.cornerRadius(for: handleHeight)) - .foregroundStyle(self.model.color.main.color) - .frame(width: handleWidth, height: handleHeight) - .overlay( - Group { - if self.model.size == .large { - RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.handleOverlaySide)) - .foregroundStyle(self.model.color.contrast.color) - .frame(width: self.model.handleOverlaySide, height: self.model.handleOverlaySide) - } + let containerHeight = self.model.containerHeight + let sliderHeight = self.model.sliderHeight + + let barWidth = self.model.barWidth(for: geometry.size.width, progress: self.progress) + let backgroundWidth = self.model.backgroundWidth(for: geometry.size.width, progress: self.progress) + + HStack(spacing: self.model.trackSpacing) { + // Progress segment + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: sliderHeight)) + .foregroundStyle(self.model.color.main.color) + .frame(width: barWidth, height: sliderHeight) + + // Handle + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.handleSize.height)) + .foregroundStyle(self.model.color.main.color) + .frame(width: self.model.handleSize.width, height: self.model.handleSize.height) + .overlay( + Group { + if self.model.size == .large { + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.handleOverlaySide)) + .foregroundStyle(self.model.color.contrast.color) + .frame(width: self.model.handleOverlaySide, height: self.model.handleOverlaySide) } - ) - .gesture( - DragGesture(minimumDistance: 0) - .onChanged { value in - let newOffset = leftWidth + value.translation.width - let clampedOffset = min(max(newOffset, 0), sliderWidth) - - self.currentValue = self.model.steppedValue(for: clampedOffset, trackWidth: sliderWidth) - } - ) - - // Remaining segment + } + ) + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { value in + let totalWidth = geometry.size.width + let sliderWidth = max(0, totalWidth - self.model.handleSize.width - 2 * self.model.trackSpacing) + + let currentLeft = barWidth + let newOffset = currentLeft + value.translation.width + + let clampedOffset = min(max(newOffset, 0), sliderWidth) + self.currentValue = self.model.steppedValue(for: clampedOffset, trackWidth: sliderWidth) + } + ) + + // Remaining segment + Group { switch self.model.style { case .light: RoundedRectangle(cornerRadius: self.model.cornerRadius(for: sliderHeight)) .foregroundStyle(self.model.backgroundColor.color) - .frame(width: max(rightWidth, 0)) + .frame(width: backgroundWidth) case .striped: ZStack { @@ -92,21 +88,18 @@ public struct SUSlider: View { .foregroundStyle(self.model.color.main.color) .cornerRadius(self.model.cornerRadius(for: sliderHeight)) } - .frame(width: max(rightWidth, 0)) + .frame(width: backgroundWidth) } } .frame(height: sliderHeight) } - .frame(width: geometry.size.width, height: containerHeight) - // Center the slider vertically within the available space - .frame(maxHeight: .infinity, alignment: .center) } + .frame(height: self.model.containerHeight) .onAppear { self.model.validateMinMaxValues() } } } - // MARK: - Helpers struct StripesShapeSlider: Shape, @unchecked Sendable { From 890627d46ea074015877c3b1803058e690fc6d24 Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Tue, 21 Jan 2025 14:17:57 +0300 Subject: [PATCH 7/8] some fix - layout extension fix - deleted unused code - deleted backgroundColor and barColor --- .../Components/Slider/Models/SliderVM.swift | 41 ++++--------------- .../Components/Slider/SUSlider.swift | 18 ++++---- 2 files changed, 16 insertions(+), 43 deletions(-) diff --git a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift index 944f1a78..18bd5900 100644 --- a/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift +++ b/Sources/ComponentsKit/Components/Slider/Models/SliderVM.swift @@ -78,23 +78,7 @@ extension SliderVM { return 4 } var handleOverlaySide: CGFloat { - 12 - } - var backgroundColor: UniversalColor { - switch style { - case .light: - return self.color.background - case .striped: - return self.color.main - } - } - var barColor: UniversalColor { - switch style { - case .light: - return self.color.main - case .striped: - return self.color.contrast - } + return 12 } private func stripesCGPath(in rect: CGRect) -> CGMutablePath { let stripeWidth: CGFloat = 2 @@ -148,31 +132,24 @@ extension SliderVM { } } -public extension SliderVM { - var sliderHeight: CGFloat { - self.trackHeight - } - +extension SliderVM { var containerHeight: CGFloat { max(self.handleSize.height, self.trackHeight) } - func leftWidth(for totalWidth: CGFloat, progress: CGFloat) -> CGFloat { - let sliderWidth = max(0, totalWidth - self.handleSize.width - 2 * self.trackSpacing) - return sliderWidth * progress - } - - func rightWidth(for totalWidth: CGFloat, progress: CGFloat) -> CGFloat { - let sliderWidth = max(0, totalWidth - self.handleSize.width - 2 * self.trackSpacing) - return sliderWidth - self.leftWidth(for: totalWidth, progress: progress) + private func sliderWidth(for totalWidth: CGFloat) -> CGFloat { + max(0, totalWidth - self.handleSize.width - 2 * self.trackSpacing) } func barWidth(for totalWidth: CGFloat, progress: CGFloat) -> CGFloat { - self.leftWidth(for: totalWidth, progress: progress) + let width = self.sliderWidth(for: totalWidth) + return width * progress } func backgroundWidth(for totalWidth: CGFloat, progress: CGFloat) -> CGFloat { - self.rightWidth(for: totalWidth, progress: progress) + let width = self.sliderWidth(for: totalWidth) + let filled = width * progress + return width - filled } } diff --git a/Sources/ComponentsKit/Components/Slider/SUSlider.swift b/Sources/ComponentsKit/Components/Slider/SUSlider.swift index 86b98f6b..a2a4083d 100644 --- a/Sources/ComponentsKit/Components/Slider/SUSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/SUSlider.swift @@ -32,17 +32,14 @@ public struct SUSlider: View { public var body: some View { GeometryReader { geometry in - let containerHeight = self.model.containerHeight - let sliderHeight = self.model.sliderHeight - let barWidth = self.model.barWidth(for: geometry.size.width, progress: self.progress) let backgroundWidth = self.model.backgroundWidth(for: geometry.size.width, progress: self.progress) HStack(spacing: self.model.trackSpacing) { // Progress segment - RoundedRectangle(cornerRadius: self.model.cornerRadius(for: sliderHeight)) + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.trackHeight)) .foregroundStyle(self.model.color.main.color) - .frame(width: barWidth, height: sliderHeight) + .frame(width: barWidth, height: self.model.trackHeight) // Handle RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.handleSize.height)) @@ -75,23 +72,22 @@ public struct SUSlider: View { Group { switch self.model.style { case .light: - RoundedRectangle(cornerRadius: self.model.cornerRadius(for: sliderHeight)) - .foregroundStyle(self.model.backgroundColor.color) + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.trackHeight)) + .foregroundStyle(self.model.color.background.color) .frame(width: backgroundWidth) - case .striped: ZStack { - RoundedRectangle(cornerRadius: self.model.cornerRadius(for: sliderHeight)) + RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.trackHeight)) .foregroundStyle(self.model.color.contrast.color) StripesShapeSlider(model: self.model) .foregroundStyle(self.model.color.main.color) - .cornerRadius(self.model.cornerRadius(for: sliderHeight)) + .cornerRadius(self.model.cornerRadius(for: self.model.trackHeight)) } .frame(width: backgroundWidth) } } - .frame(height: sliderHeight) + .frame(height: self.model.trackHeight) } } .frame(height: self.model.containerHeight) From 058dd6761884909fcde98456875208d1407de3ce Mon Sep 17 00:00:00 2001 From: Vislov Ivan Date: Tue, 21 Jan 2025 14:31:25 +0300 Subject: [PATCH 8/8] .foregroundStyle fix - fix for case .striped (color .clear) --- 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 a2a4083d..76f3b262 100644 --- a/Sources/ComponentsKit/Components/Slider/SUSlider.swift +++ b/Sources/ComponentsKit/Components/Slider/SUSlider.swift @@ -78,7 +78,7 @@ public struct SUSlider: View { case .striped: ZStack { RoundedRectangle(cornerRadius: self.model.cornerRadius(for: self.model.trackHeight)) - .foregroundStyle(self.model.color.contrast.color) + .foregroundStyle(.clear) StripesShapeSlider(model: self.model) .foregroundStyle(self.model.color.main.color)