Q:

Interlinked Multiple Sliders in SwiftUI

I was wondering whether anyone could help me.
In the app I'm working on, I need to split a value into four different areas and I'd like to do so via four sliders, which control the percentage for each of the areas.
For each Slider, I've created an @State private var sliderAValue = 25 and bound them to the sliders via Slider(value: $sliderAValue, in: 0...100)
Clearly, when I change the value of any of the sliders, the other values should also change in order that the total percentage is always 100.
But more importantly, the ratio between the other three values should state the same: eg. if the original split is 30 : 30 : 20 : 20 and the user changes the third element to 40%, then the new split should become 22.5 : 22.5 : 40 : 15.
Applying a didSet or a willSet on the State variables doesn't seem to have any effect, yet I clearly need to store the original ratios somewhere and have them automatically updated.  
Any ideas?

A:

You can store the bindings to all State variables in an array of type Array<Binding<Double>>. This allows you to add more sliders if you want without much more code.

struct Sliders: View {

    @State var value1 = 100.0
    @State var value2 = 0.0
    @State var value3 = 0.0
    @State var value4 = 0.0

    var body: some View {

        // add all bindings which you want to synchronize
        let allBindings = [$value1, $value2, $value3, $value4]

        return VStack {

            Button(action: {
                // Manually setting the values does not change the values such
                // that they sum to 100. Use separate algorithm for this
                self.value1 = 10
                self.value2 = 40
            }) {
                Text("Test")
            }

            Text("\(value1)")
            synchronizedSlider(from: allBindings, index: 0)

            Text("\(value2)")
            synchronizedSlider(from: allBindings, index: 1)

            Text("\(value3)")
            synchronizedSlider(from: allBindings, index: 2)

            Text("\(value4)")
            synchronizedSlider(from: allBindings, index: 3)

        }.padding()
    }

    func synchronizedSlider(from bindings: [Binding<Double>], index: Int) -> some View {
        return Slider(value: synchronizedBinding(from: bindings, index: index),
                      in: 0...100)
    }

    func synchronizedBinding(from bindings: [Binding<Double>], index: Int) -> Binding<Double> {

        return Binding(get: {
            return bindings[index].wrappedValue
        }, set: { newValue in

            let sum = bindings.indices.lazy.filter{ $0 != index }.map{ bindings[$0].wrappedValue }.reduce(0.0, +)
            // Use the 'sum' below if you initially provide values which sum to 100
            // and if you do not set the state in code (e.g. click the button)
            //let sum = 100.0 - bindings[index].wrappedValue

            let remaining = 100.0 - newValue

            if sum != 0.0 {
                for i in bindings.indices {
                    if i != index {
                        bindings[i].wrappedValue = bindings[i].wrappedValue * remaining / sum
                    }
                }
            } else {
                // handle 0 sum
                let newOtherValue = remaining / Double(bindings.count - 1)
                for i in bindings.indices {
                    if i != index {
                        bindings[i].wrappedValue = newOtherValue
                    }
                }
            }
            bindings[index].wrappedValue = newValue
        })

    }

}

If you want to synchronize only the sliders for value1/value2 and value3/value4 you can change the code in body to:
let bindings12 = [$value1, $value2]
let bindings34 = [$value3, $value4]

return VStack {

    Text("\(value1)")
    synchronizedSlider(from: bindings12, index: 0)

    Text("\(value2)")
    synchronizedSlider(from: bindings12, index: 1)

    Text("\(value3)")
    synchronizedSlider(from: bindings34, index: 0)

    Text("\(value4)")
    synchronizedSlider(from: bindings34, index: 1)

}.padding()

