How to stop strobing with fast moving view in SwiftUI

Viewed 88

I'm trying to create a slider in SwiftUI (in the same style as the native Slider, but ultimately with more functionality). In the below gif I've got my slider on top and SwiftUI's native slider on the bottom, both with the same range and value. When I drag the native slider around, my view updates but the circle strobes when I drag back and forward very quickly. Is there a way to prevent this strobing please? The gif's not that clear so I've provided the code to reproduce. Any pointers in the right direction appreciated!

The strobing effect. It's not that clear due to the frame rate of gifs.

Here's the code for my slider:

struct BarSlider: View {

    @Binding var value: CGFloat
    var range: ClosedRange<CGFloat>
    
    var body: some View {

        GeometryReader { geo in
            ZStack {
                HStack(spacing: 0) {
                    Rectangle()
                        .fill(Color.red)
                        .frame(width: $value.wrappedValue.map(from: range, to: 0...geo.size.width))
                    Rectangle()
                        .fill(Color.green)
                }
                HStack {
                    Circle()
                        .frame(width: 30, height: 30)
                        .offset(x: valueForKnob(geometry: geo))
                    Spacer()
                }
            }
            .background(Color.blue)
        }
    }

    private func valueForKnob(geometry: GeometryProxy) -> CGFloat {
        $value.wrappedValue.map(from: range, to: 0...geometry.size.width) - (30 * valueAsPercent())
    }

    private func valueAsPercent() -> CGFloat {
        let percent = ((value - range.lowerBound) / (range.upperBound - range.lowerBound) * 100) / 100
        return percent.clamped(to: 0...1)
    }
}

And here's both sliders:

struct ContentView: View {
    @State private var amount = CGFloat(100)

    var body: some View {
        VStack {
            BarSlider(value: $amount, range: 100...1000)
                .frame(height: 100)
            Slider(value: $amount, in: 100...1000)
        }
    }
}

And these extensions:

extension CGFloat {
    func map(from: ClosedRange<CGFloat>, to: ClosedRange<CGFloat>) -> CGFloat {

        let value = self.clamped(to: from)

        let fromRange = from.upperBound - from.lowerBound
        let toRange = to.upperBound - to.lowerBound
        let result = (((value - from.lowerBound) / fromRange) * toRange) + to.lowerBound
        return result
    }
}

extension Comparable {
    func clamped(to limits: ClosedRange<Self>) -> Self {
        return min(max(self, limits.lowerBound), limits.upperBound)
    }
}

1 Answers

I think your circle is not in the center

please test it:

struct BarSlider: View {

    @Binding var value: CGFloat
    var range: ClosedRange<CGFloat>
    
    private let circleSize:CGFloat = 20
    
    var body: some View {

        GeometryReader { geo in
            VStack {
                Text("\(value)")
                ZStack (alignment: Alignment(horizontal: .leading, vertical: .center)){
                    HStack(spacing: 0) {
                        Rectangle()
                            .fill(Color.red)
                            .frame(width: $value.wrappedValue.map(from: range, to: 0...geo.size.width))
                            
                        Rectangle()
                            .fill(Color.green)
                    }
                    
                    HStack {
                        Circle()
                            .frame(width: circleSize, height: circleSize)
                            .offset(x: valueForKnob(geometry: geo) - circleSize/2)
                       
                    }
                }
            }
            
        }
    }

    private func valueForKnob(geometry: GeometryProxy) -> CGFloat {
        $value.wrappedValue.map(from: range, to: 0...geometry.size.width )
    }

    private func valueAsPercent() -> CGFloat {
        let percent = ((value - range.lowerBound) / (range.upperBound - range.lowerBound) * 100) / 100
        return percent.clamped(to: 0...1)
    }
}
extension CGFloat {
    func map(from: ClosedRange<CGFloat>, to: ClosedRange<CGFloat>) -> CGFloat {

        let value = self.clamped(to: from)

        let fromRange = from.upperBound - from.lowerBound
        let toRange = to.upperBound - to.lowerBound
        let result = (((value - from.lowerBound) / fromRange) * toRange) + to.lowerBound
        return result
    }
}
extension Comparable {
    func clamped(to r: ClosedRange<Self>) -> Self {
        let min = r.lowerBound, max = r.upperBound
        return self < min ? min : (max < self ? max : self)
    }
}


struct ContentView: View {
    @State private var amount = CGFloat(100)

    var body: some View {
        VStack {
            BarSlider(value: $amount, range: 100...1000)
                .frame(height: 70)
                .padding()
            Spacer()
                .frame(height: 70)
            Slider(value: $amount, in: 100...1000)
                .padding()
        }
    }
}

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