Search code examples
iosswiftswiftuislider

Why won't a SwiftUI Slider trigger a change to a small range?


I am trying to make a slider update a value if it changes. In this case, I'm looking for a lower and upper bound in a range from 0 to 4. If the min value is greater than the max value, I want the max to update (to equal the min value). I want the same to happen in reverse - if the max is slid to less than the min, the min should automatically update to compensate.

import SwiftUI

struct ContentView: View {
    @State private var numPeople: [Double] = [0, 2]
    
    var body: some View {
        VStack {
            Text("Hello, world!")
            
            Section(header: Text("Number of People")) {
                createEditableRangeSection(values: $numPeople, range: 0...4)
            }
        }
        .padding()
    }
    
    func createEditableRangeSection(values: Binding<[Double]>, range: ClosedRange<Double>) -> some View {
        VStack {
            HStack {
                Text("Min: \((values.wrappedValue[0]))")
                Slider(value: values[0], in: range, step: 1)
                    .onChange(of: values.wrappedValue[0]) { newValue, _ in
                        if Int(newValue) >= 3 {
                            print (newValue, values.wrappedValue[1])
                        }
                        if newValue > values.wrappedValue[1] {
                            values.wrappedValue[1] = newValue
                        }
                    }
            }
            HStack {
                Text("Max: \((values.wrappedValue[1]))")
                Slider(value: values[1], in: range, step: 1)
                    .onChange(of: values.wrappedValue[1]) { newValue, _ in
                        if newValue < values.wrappedValue[0] {
                            values.wrappedValue[0] = newValue
                        }
                    }
            }
        }
    }
}

This code works perfectly with a large range (like 20...200) but for the range of 0...4 it has an interesting effect. You can move the min to the value of (max + 1) and release it, and it looks like this:

Slider with incorrect behavior in the VisionOS Simulator

However, if you grab it and slowly move it back to the previous position (max) the max value will suddenly update and jump to the right.


Solution

  • The first parameter of the onChange closure is the old value, not the new value!

    You should change

    .onChange(of: ...) { newValue, _ in ... }
    

    to

    .onChange(of: ...) { _, newValue in ... }