tHatpart
tHatpart

Reputation: 1146

SwiftUI Slider Thumb Image Offset

I am trying to make a custom SwiftUI Slider with ticks and a custom thumb image. It works well except for that the thumb image is not aligned with the tick marks. How can I make them aligned?

init() {
    let ticker: UIImage = UIImage(named: "ticker")?.withAlignmentRectInsets(UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 90)) ?? UIImage()
    UISlider.appearance().setThumbImage(ticker, for: .normal)
}

Slider:

 ZStack {
      HStack { 
           ForEach(0...10, id: \.self) { _ in
               Text("I")
                   .frame(maxWidth: .infinity)
                   .foregroundColor(ColorStyle(colorAsset: .lightGrey).color)
            
      }
      Slider(value: $viewModel.sliderValue, in: minSlider...maxSlider, step: sliderStep)
           .tint(ColorStyle(colorAsset: .lightGrey).color)
}             

Upvotes: 0

Views: 425

Answers (1)

bmrptm
bmrptm

Reputation: 1

working on a generic slider builder to reduce repeated coder, optimised for iPad (iOS). The trick is to allow for the way the track length is reduced by the thumb width, so full range is a little less than the frame width. The attached code is incomplete - need to set up @environment for passed in variables. The pink borders are just to highlight the frames for you.

    import SwiftUI

fileprivate let panelFrameWidth: CGFloat = 500
fileprivate let sliderFrameWidth: CGFloat = 400
fileprivate let sliderFrameHeight: CGFloat = 70
fileprivate let sliderTitleWidth: CGFloat = panelFrameWidth - sliderFrameWidth
fileprivate let sliderValueYoffset: CGFloat = -30
fileprivate let sliderTickYoffset: CGFloat = 7.5
fileprivate let sliderCallupYoffset: CGFloat = 15
fileprivate let thumbWidth: CGFloat = 26 // 26 for iPad & iPhone; 6 for MacOS
fileprivate let tickMark = "I" // character for tick mark on slider
fileprivate let dp0 = "%.0f" // for formatting float in Text() to 0 decimal places
fileprivate let dp1 = "%.1f" // for formatting float in Text() to 1 decimal places
fileprivate let dp2 = "%.2f" // for formatting float in Text() to 2 decimal places

struct SliderBuilderData {
    var title: String = "Title"
    var rangeMin: Double = 0
    var rangeMax: Double = 100
    var rangeSteps: Double = 10
    var rangeCallups: [Double] = [0,25,50,75,100]
    var valueToPxl: Double {(sliderFrameWidth - thumbWidth)/(rangeMax - rangeMin)}
    var rangeCallupCentreOffsets: [CGFloat] {rangeCallups.map {CGFloat((($0) - (rangeMin + rangeMax)/2)*valueToPxl)}}
    var rangeCallupsToDisplay: [String] {rangeCallups.map {String(format: dp0, ($0))}}
}

struct SliderValueText: View {
    let text: String

    var body: some View {
        Text(text)
            .font(.body)
            .padding(2.0)
            .foregroundColor(.white)
            .background(.blue)
            .clipShape(Capsule())
    }
}

struct SliderViewBuilder: View {
    @State var value: Double
    let slider: SliderBuilderData

    var body: some View {
        ZStack {
            HStack (spacing: 0){
                Text(slider.title)
                    .frame(width: sliderTitleWidth, height: sliderFrameHeight, alignment: .leading)
                    .border(Color.pink)
            
                Slider(value: $value, in: slider.rangeMin...slider.rangeMax, step: slider.rangeSteps)
                    .frame(width: sliderFrameWidth, height: sliderFrameHeight)
                    .border(Color.pink)
            }
            SliderValueText(text: String(format: dp0, value))
                .frame(width: sliderFrameWidth - thumbWidth - 5, height: sliderFrameHeight/2, alignment: .center)
                .offset(x: sliderTitleWidth/2 + (value - (slider.rangeMin + slider.rangeMax)/2)*slider.valueToPxl, y: sliderValueYoffset)
            ForEach(slider.rangeCallups.indices, id: \.self) {index in
                VStack {
                    Text(tickMark) // this is for tick marks; omit for MacOS
                        .font(.caption)
                        .offset(x: sliderTitleWidth/2 + slider.rangeCallupCentreOffsets[index], y: sliderTickYoffset)
                    Text("\(slider.rangeCallupsToDisplay[index])")
                        .font(.caption)
                        .offset(x: sliderTitleWidth/2 + slider.rangeCallupCentreOffsets[index], y:sliderCallupYoffset)
                }
            }
        }
    }
}

struct ContentView: View {
    @State var testValue: Double = 25
    @State var cover: Double = 30

    let testBuilder: SliderBuilderData = SliderBuilderData()
    let coverSliderBuilder: SliderBuilderData = SliderBuilderData(title: "Cover\n(mm)", rangeMin: 20, rangeMax: 80, rangeSteps: 5, rangeCallups: [20,30,40,50,60,70,80])

    var body: some View {
        VStack {
            SliderViewBuilder(value: testValue, slider: testBuilder)
            Text("Hello, World! \(String(format: dp2, testValue))")
            SliderViewBuilder(value: cover, slider: coverSliderBuilder)
        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
} 

Upvotes: 0

Related Questions