Home > Software design >  Object Color not Updating when Changing Custom Slider Value in SwiftUI
Object Color not Updating when Changing Custom Slider Value in SwiftUI

Time:07-12

I have an app built in SwiftUI that uses a custom slider to change a value. This value controls the color of a rectangle. When using the slider, it properly changes the value but the color of the rectangle does not update. It does work normally with a normal built-in slider.

Here is the code for the view

struct SliderTestView: View {
    
    @State var redValue = 0.0
    @State var greenValue = 0.5
    @State var blueValue = 0.5
    
    var body: some View {
        VStack {
            Text("Hello, World! The red value is: \(redValue)")
            Rectangle()
                .foregroundColor(Color(red: redValue, green: greenValue, blue: blueValue, opacity: 1.0))
                .cornerRadius(10)
            CSlider(value: $redValue)
        }
        .padding()
        
    }
}

struct CSlider: View {
        
    @Binding var value: Double
    
    var body: some View {
        CustomSlider(value: $value, range: (0, 100)) { modifiers in
            ZStack {
                LinearGradient(gradient: .init(colors: [Color.pink, Color.red]), startPoint: .leading, endPoint: .trailing)
                ZStack {
                    Circle().fill(Color.white)
                    Circle().stroke(Color.black.opacity(0.2), lineWidth: 2)
                    Text(("\(Int(self.value))")).font(.system(size: 11))
                }
                .padding([.top, .bottom], 2)
                .modifier(modifiers.knob)
            }.cornerRadius(15)
        }.frame(height: 30)
    }
    
}

And here is the logic code for the custom slider.

struct CustomSlider<Component: View>: View {
    
    @Binding var value: Double
    var range: (Double, Double)
    var knobWidth: CGFloat?
    let viewBuilder: (CustomSliderComponents) -> Component
    
    init(value: Binding<Double>, range: (Double, Double), knobWidth: CGFloat? = nil, _ viewBuilder: @escaping (CustomSliderComponents) -> Component
    ) {
        _value = value
        self.range = range
        self.viewBuilder = viewBuilder
        self.knobWidth = knobWidth
    }
    
    
    
    var body: some View {
        return GeometryReader { geometry in
            self.view(geometry: geometry) // function below
        }
    }
    
    private func view(geometry: GeometryProxy) -> some View {
        let frame = geometry.frame(in: .global)
        let drag = DragGesture(minimumDistance: 0).onChanged({ drag in
            self.onDragChange(drag, frame) }
        )
        let offsetX = self.getOffsetX(frame: frame)
        
        let knobSize = CGSize(width: knobWidth ?? frame.height, height: frame.height)
        let barLeftSize = CGSize(width: CGFloat(offsetX   knobSize.width * 0.5), height:  frame.height)
        let barRightSize = CGSize(width: frame.width - barLeftSize.width, height: frame.height)
        
        let modifiers = CustomSliderComponents(
            barLeft: CustomSliderModifier(name: .barLeft, size: barLeftSize, offset: 0),
            barRight: CustomSliderModifier(name: .barRight, size: barRightSize, offset: barLeftSize.width),
            knob: CustomSliderModifier(name: .knob, size: knobSize, offset: offsetX))
        
        return ZStack { viewBuilder(modifiers).gesture(drag) }
    }
    
    private func onDragChange(_ drag: DragGesture.Value,_ frame: CGRect) {
        let width = (knob: Double(knobWidth ?? frame.size.height), view: Double(frame.size.width))
        let xrange = (min: Double(0), max: Double(width.view - width.knob))
        var value = Double(drag.startLocation.x   drag.translation.width) // knob center x
        value -= 0.5*width.knob // offset from center to leading edge of knob
        value = value > xrange.max ? xrange.max : value // limit to leading edge
        value = value < xrange.min ? xrange.min : value // limit to trailing edge
        value = value.convert(fromRange: (xrange.min, xrange.max), toRange: range)
        self.value = value
        print("KNOB IS MOVING. The value is: \(self.value)")
    }
    
    private func getOffsetX(frame: CGRect) -> CGFloat {
        let width = (knob: knobWidth ?? frame.size.height, view: frame.size.width)
        let xrange: (Double, Double) = (0, Double(width.view - width.knob))
        let result = self.value.convert(fromRange: range, toRange: xrange)
        return CGFloat(result)
    }
}

struct CustomSliderComponents {
    let barLeft: CustomSliderModifier
    let barRight: CustomSliderModifier
    let knob: CustomSliderModifier
}
struct CustomSliderModifier: ViewModifier {
    enum Name {
        case barLeft
        case barRight
        case knob
    }
    let name: Name
    let size: CGSize
    let offset: CGFloat
    
    func body(content: Content) -> some View {
        content
            .frame(width: size.width)
            .position(x: size.width*0.5, y: size.height*0.5)
            .offset(x: offset)
    }
}




//MARK: - Double Ext.
extension Double {
    func convert(fromRange: (Double, Double), toRange: (Double, Double)) -> Double {
        // Example: if self = 1, fromRange = (0,2), toRange = (10,12) -> solution = 11
        var value = self
        value -= fromRange.0
        value /= Double(fromRange.1 - fromRange.0)
        value *= toRange.1 - toRange.0
        value  = toRange.0
        return value
    }
}

CodePudding user response:

Your problem is that the redValue being set by your slider is a value in the range 0.0...100.0, but you need a value in the range 0.0...1.0 when setting the color of the Rectangle:

Rectangle()
    .foregroundColor(Color(red: redValue/100, green: greenValue, blue: blueValue, opacity: 1.0))
    .cornerRadius(10)
  • Related