I'm trying to create a range slider for a person's height where the minimum height of 3ft shall also be the starting point of the range slider.
The code below shows a range slider with a starting point of 0ft how can I modify it to start at 3ft
Expected Range Slider Design
Current Range Slider
Code for Current Range Slider
import SwiftUI
struct ContentView: View {
@State var heightMinValue: Double = 0.4 * Constants.Range.size // 40% of Constants.Range.size
@State var heightMaxValue: Double = Constants.Range.size
var body: some View {
VStack {
HeightRangeSliderView(scaleMinValue: $heightMinValue, scaleMaxValue: $heightMaxValue, minimumpercentageVal: 40, lableWidth: 89)
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct HeightRangeSliderView: View {
/// ` Slider` Binding min & max values
@Binding var scaleMinValue: Double
@Binding var scaleMaxValue: Double
var minimumpercentageVal: Double
var leftKnobWidth: Double = 28
var leftKnobHeight: Double = 28
var rightKnobWidth: Double = 28
var rightKnobHeight: Double = 28
var barWidth: Double = Constants.Range.size
var leftKnobColor = Color.white
var rightKnobColor = Color.white
var backgroundTrackColor = Color.DesignSystem.Divider
var selectedTrackColor = Color.DesignSystem.Active
/// Slider min & max static and dynamic labels value color
var sliderMinMaxValuesColor = Color.DesignSystem.Active
/// Set slider min & max Label values
var lableWidth: Double = 55
var lableColor = Color.DesignSystem.FieldBg
var minLabel: String {
String(format: "%.0f", (CGFloat(scaleMinValue) / barWidth) * 100)
}
var maxLabel: String {
String(format: "%.0f", (CGFloat(scaleMaxValue) / barWidth) * 100)
}
private let heightformatStyle = Measurement<UnitLength>.FormatStyle(width: .abbreviated, usage: .personHeight)
var heightMinValue: Measurement<UnitLength> {
Measurement(value: (scaleMinValue / barWidth) * 100, unit: UnitLength.inches)
}
var heightMaxValue: Measurement<UnitLength> {
Measurement(value: (scaleMaxValue / barWidth) * 100, unit: UnitLength.inches)
}
var body: some View {
VStack {
/// `Slider` start & end static values show in view
HStack {
// start value
Text(heightMinValue, format: heightformatStyle)
.monospacedDigit()
.frame(width: lableWidth, height: 40, alignment: .center)
.foregroundColor(sliderMinMaxValuesColor)
.background(lableColor, in: RoundedRectangle(cornerRadius: 8))
Spacer()
// end value
Text(heightMaxValue, format: heightformatStyle)
.monospacedDigit()
.frame(width: lableWidth, height: 40, alignment: .center)
.foregroundColor(sliderMinMaxValuesColor)
.background(lableColor, in: RoundedRectangle(cornerRadius: 8))
}
.frame(width: (barWidth + (rightKnobWidth * 2)))
.padding(.horizontal)
/// `Slider` track view
ZStack(alignment: .leading) {
// background track view
Capsule()
.fill(backgroundTrackColor)
.frame(width: (barWidth + (rightKnobWidth * 2)), height: 6)
// selected track view
Capsule()
.fill(selectedTrackColor)
.frame(width: scaleMaxValue - scaleMinValue, height: 6)
.offset(x: scaleMinValue + rightKnobWidth)
HStack(spacing: 0) {
// minimum value glob view
Circle()
.fill(leftKnobColor)
.shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 6)
.shadow(color: Color.black.opacity(0.2), radius: 4, x: 0, y: 0.5)
.frame(width: leftKnobWidth, height: leftKnobHeight)
.offset(x: scaleMinValue)
.gesture(DragGesture().onChanged({ value in
/// drag validation
if value.location.x >= 0 && value.location.x <= scaleMaxValue {
scaleMinValue = value.location.x
}
})
)
// maximum value glob view
Circle()
.fill(rightKnobColor)
.shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 6)
.shadow(color: Color.black.opacity(0.2), radius: 4, x: 0, y: 0.5)
.frame(width: rightKnobWidth, height: rightKnobHeight)
.offset(x: scaleMaxValue)
.gesture(DragGesture().onChanged({ value in
/// drag validation
if value.location.x <= barWidth && value.location.x >= scaleMinValue {
scaleMaxValue = value.location.x
}
})
)
}
}
}
}
}
enum Constants {
enum Range {
static let size = UIScreen.main.bounds.width - 100
}
}
extension Color {
enum DesignSystem {
public static let Active = Color.green
public static let FieldBg = Color.black
public static let Divider = Color.gray
}
}
I have made this one open-source... you can check out its code as it implements very much what you are looking for github.com/diegotid/circular-range-slider
The slider components must have both the selected range and the bounds as separate parameters and map them
public struct CircularRangeSlider: View {
@Binding var range: ClosedRange<Double>
var bounds: ClosedRange<Double>
Then add something to remap if bounds change
.onChange(of: bounds) { oldValue, _ in
clampRangeIfNeeded(fromPriorBounds: oldValue)
}
And finally, update the range parameter on dragging...
DragGesture()
.onChanged { value in
let angle: Angle = angleFromDrag(location: value.location)
if draggingHandle == handle || draggingHandle == nil {
if let last: Angle = lastHapticAngle {
if abs(angle.degrees - last.degrees) > 4 {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
lastHapticAngle = angle
}
} else {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
lastHapticAngle = angle
}
var arcMoves: Bool = true
var newLower: Double = range.lowerBound
var newUpper: Double = range.upperBound
if [.start, .arc].contains(handle) {
if draggingHandle == nil {
startDragOffset = Angle(degrees: angle.degrees - rangeDegrees.lowerBound)
}
newLower = moveStartHandleResultingInValue(to: angle)
arcMoves = arcMoves
&& newLower != range.lowerBound
&& newLower > bounds.lowerBound
}
if [.end, .arc].contains(handle) {
if draggingHandle == nil {
endDragOffset = Angle(degrees: angle.degrees - rangeDegrees.upperBound)
}
newUpper = moveEndHandleResultingInValue(to: angle)
arcMoves = arcMoves
&& newUpper != range.upperBound
&& newUpper < bounds.upperBound
}
if handle != .arc || arcMoves {
newLower = snapToStep(newLower, for: .start)
newUpper = snapToStep(newUpper, for: .end)
self.range = newLower...newUpper
}
if draggingHandle == nil {
draggingHandle = handle
}
}
}
.onEnded { _ in
draggingHandle = nil
lastHapticAngle = nil
}