3

I am looking at creating a custom slider, with a gradient background, numbers to represent the values, and white dots on the line to show where each step is, what I'm looking for looks like this:

Slider Image

I have looked at several other tutorials but none of what I do gives me the result Im after, either because elements don't line up, or my code doesn't work as well as the default slider.

An extra bit of trickiness is I also want to have the snap to position like the default slider, so a user can't select a value of 6.5522, only 6 or seven.

Here is the code I currently have:

import SwiftUI

struct CustomSlider: View {
let textColor: Color
let thumbColor: Color
let height: CGFloat
let cornerRadius: CGFloat

@State var lastOffset: CGFloat = 0

@Binding var value: CGFloat

var range: ClosedRange<CGFloat>

var leadingOffset: CGFloat = 5
var trailingOffset: CGFloat = 5



var body: some View {
    GeometryReader { geo in
        VStack {
            ZStack {
                RoundedRectangle(cornerRadius: cornerRadius).fill(LinearGradient(gradient: Gradient(colors: [.paleRed, .mango, .neonYellow, .midGreen]), startPoint: .leading, endPoint: .trailing))
                    .frame(height: height)
                HStack {
                    ForEach(1..<11) { index in
                        Circle()
                            .foregroundColor(.white)
                            .frame(width: height, height: height)
                        if index < 10 {
                            Spacer()
                        }
                    }
                }
                HStack {
                    Circle()
                        .frame(width: height * 2, height: height * 2)
                        .foregroundColor(thumbColor)
                        .offset(x: CGFloat(self.$value.wrappedValue.map(from: self.range, to: 6...(geo.size.width - 6 - 22))))
                        .gesture(
                            DragGesture(minimumDistance: 0)
                                .onChanged { value in

                                    if abs(value.translation.width) < 0.1 {
                                        self.lastOffset = self.$value.wrappedValue.map(from: self.range, to: self.leadingOffset...(geo.size.width - (height * 2) - self.trailingOffset))
                                    }

                                    let sliderPos = max(0 + self.leadingOffset, min(self.lastOffset + value.translation.width, geo.size.width - (height * 2) - self.trailingOffset))
                                    let sliderVal = sliderPos.map(from: self.leadingOffset...(geo.size.width - (height * 2) - self.trailingOffset), to: self.range)

                                    self.value = sliderVal
                                }
                      )
                    Spacer()
                }
            }
            HStack {
                ForEach(1..<11) { index in
                    Text("\(index)")
                        .foregroundColor(.secondaryButtonLightGrey)
                        .font(.custom("Rubik-Medium", size: 16))
                    if index < 10 {
                        Spacer()
                    }
                }
            }
        }


    }
}
}

// How its implemented in the view code:

CustomSlider(textColor: .textColor, thumbColor: .thumbColor, height: 10, cornerRadius: 10, value: $value, range: 0...10)

I apologize if these are really simple questions but I am still new to swiftUI, only been using it for a month.

Thank you for any advice.

0

2 Answers 2

2

Hopefully this may help:

    struct CustomSliderView: View {
    @State var slider: Float = 0
    @State var dragGestureTranslation: CGFloat = 0
    @State var lastDragValue: CGFloat = 0
    
    // Slider Draggable Control Settings
    var sliderWidth: CGFloat = 30
    var sliderPadding: CGFloat = 0 // This adds padding to each side
    
    // Stepped Increment
    @State var steppedSlider: Bool = false
    @State var step: Int = 8
    @State var stepInterval: CGFloat = 0
    
    func interval(width: CGFloat, increment: Int) -> CGFloat {
        print("Screen Width: \(width)")
        let result = width * CGFloat(increment) / CGFloat(step)
        return result
    }
    
    func roundToFactor(value: CGFloat, factor: CGFloat) -> CGFloat {
        return factor * round(value / factor)
    }
    
    var body: some View {
        VStack {
            GeometryReader { geometry in
                // Slider Bar
                ZStack (alignment: .leading) {
                    Rectangle()
                        .foregroundColor(.blue)
                } // End of Slider Bar
                .frame(width: geometry.size.width, height: 3)
                .padding(.vertical, 15) // Padding to centre the bar
                
                // Slider Stacker
                ZStack (alignment: .top) {
                    if steppedSlider {
                        // Step - Minused the SliderWidth so that it is evenly spaced
                        ForEach(0...step, id: \.self) { number in
                            Rectangle()
                                .frame(width: 2, height: 10)
                                .offset(x: interval(width: (geometry.size.width - sliderWidth), increment: number), y: 46)
                            
                            Text("\(number)")
                                .fontWeight(.bold)
                                .offset(x: interval(width: (geometry.size.width - sliderWidth), increment: number), y: 64)
                                
                        }
                    }
                    
                    // Draggable Slider
                    ZStack {
                        Circle()
                            .frame(width: sliderWidth, height: 30)
                    }
                    .offset(x: CGFloat(slider))
                    .padding(.horizontal, sliderPadding)
                } // End of Slider ZStack
                .gesture(DragGesture(minimumDistance: 0)
                            .onChanged({ dragValue in
                                let translation = dragValue.translation
                                
                                dragGestureTranslation = CGFloat(translation.width) + lastDragValue
                                
                                // Set the start marker of the slider
                                dragGestureTranslation = dragGestureTranslation >= 0 ? dragGestureTranslation : 0
                                
                                // Set the end marker of the slider
                                dragGestureTranslation = dragGestureTranslation > (geometry.size.width - sliderWidth - sliderPadding * 2) ? (geometry.size.width - sliderWidth - sliderPadding * 2) :  dragGestureTranslation
                                
                                // Set the slider value (Stepped)
                                if steppedSlider {
                                    // Getting the stepper interval (where to place the marks)
                                    stepInterval = roundToFactor(value: dragGestureTranslation, factor: (geometry.size.width - sliderWidth - (sliderPadding * 2)) / CGFloat(step))
                                    
                                    // Get the increments for the stepepdInterval
                                    self.slider = min(max(0, Float(stepInterval)), Float(stepInterval))
                                } else {
                                    // Set the slider value (Fluid)
                                    self.slider = min(max(0, Float(dragGestureTranslation)), Float(dragGestureTranslation))
                                }

                            })
                            .onEnded({ dragValue in
                                // Set the start marker of the slider
                                dragGestureTranslation = dragGestureTranslation >= 0 ? dragGestureTranslation : 0
                                
                                // Set the end marker of the slider
                                dragGestureTranslation = dragGestureTranslation > (geometry.size.width - sliderWidth - sliderPadding * 2) ? (geometry.size.width - sliderWidth - sliderPadding * 2) : dragGestureTranslation
                                
                                // Storing last drag value
                                lastDragValue = dragGestureTranslation
                            })
                ) // End of DragGesture
            } // End of GeometryReader
            Text("Slider: \(slider)")
        } // End of VStack
        .padding()
    }
}
Sign up to request clarification or add additional context in comments.

Comments

0

I can answer to the second part of your question. To get the Integer value, you just have to cast the value. For example:

self.value = Int(sliderVal)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.