swiftuiviewslidermodifier

Update SwiftUI Slider with EnvironmentObject


I am trying to get the position of the slider thumb to respond to changes coming from a TextField in another view through a common EnvironmentObject (inputs). The communication works fine from Slider to Inputs on to TextField. And the initial state of TextField is communicated to the Slider thumb through the .onAppear modifier and that works. So far so good. However, subsequent changes made in the TextField only show up in the Text view within the SliderView in the slider. Here's a screen shot of the inconsistent thumb position and text output

Inconsistent State

Notice the position of the thumb. It remains at position 4 which was its initial position.

I added a .onTapGesture to the Text view within the Slider and that works. Thumb moves to the position consistent with the input. Here is after the tap

After tapping slider text

I have been unable to find any modifier that will forces the thumb to respond to the change coming from the TextField. Since the .onTapGesture does work, I'm confident that the information is available to the slider, just can't get it to respond on it's own and the tapGesture solution seems klunky. I tried various modifiers All of the necessary code is below. It seems as if the change in the EnvironmentObject is not prompting the Slider to update.


//
//  SliderExample.swift
//  MapTests
//
//  Created by Thomas Mead on 1/20/24.
//
import SwiftUI
import Foundation

struct SliderExample: View {
    @EnvironmentObject var inputs : Inputs
    var body: some View {
        Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
        VStack {
            InputView()
            SliderView()
        }
    }
}

struct InputView: View {
    @EnvironmentObject var inputs: Inputs
    var body: some View {
        HStack {
            Text("Sample Number")
            TextField("SampleNumber", value: $inputs.sampleNumber, format: .number)
        }
    }
}

struct SliderView: View {
    @EnvironmentObject var inputs: Inputs
    @State var testNumber = 1.0
    var body: some View {
        HStack {
            Slider(value: $testNumber, in: 1...20.0, step: 1.0)
              .onChange(of: testNumber, perform: {data in inputs.sampleNumber =        Int(testNumber)} )
            .onAppear() {testNumber = Double(inputs.sampleNumber)}
        
            Text("Sample \(Int(inputs.sampleNumber))")
                .onTapGesture {
                    testNumber = Double(inputs.sampleNumber)
                }
            }
        }
    }

class Inputs: Observable, ObservableObject {
    init() {
    }
        @Published var sampleNumber: Int = 4
}


#Preview {
    SliderExample()
}

Solution

  • The reason your Slider does not update is because you do not update the testNumber when the sampleNumber of the model changes.

    Try this approach using a .onChange() in your SliderView

    Note, as mentioned, you should not have Observable as well as ObservableObject in your class Inputs.

    struct ContentView: View {
        @StateObject var inputs = Inputs()
        
        var body: some View {
            SliderExample()
                .environmentObject(inputs)
        }
    }
    
    struct SliderExample: View {
        @EnvironmentObject var inputs : Inputs
        
        var body: some View {
            Text("Hello, World!")
            VStack {
                InputView()
                   .border(.red) // <-- for testing
                SliderView()
            }
        }
    }
    
    struct InputView: View {
        @EnvironmentObject var inputs: Inputs
        
        var body: some View {
            HStack {
                Text("Sample Number")
                TextField("SampleNumber", value: $inputs.sampleNumber, format: .number)
            }
        }
    }
    
    struct SliderView: View {
        @EnvironmentObject var inputs: Inputs
        @State var testNumber = 1.0
        
        var body: some View {
            HStack {
                Slider(value: $testNumber, in: 1...20.0, step: 1.0)
                    .onChange(of: testNumber) {  // <--- here
                        inputs.sampleNumber = Int(testNumber)
                    }
                    .onAppear() {
                        testNumber = Double(inputs.sampleNumber)
                    }
                
                Text("Sample \(Int(inputs.sampleNumber))")
                    .onTapGesture {
                        testNumber = Double(inputs.sampleNumber)
                    }
            }
            .onChange(of: inputs.sampleNumber) {  // <--- here
                testNumber = Double(inputs.sampleNumber)
            }
        }
    }
    
    class Inputs: ObservableObject { // <-- here  no  Observable,
        @Published var sampleNumber: Int = 4
    }
    

    Of course you can simplify your code by using @Published var sampleNumber: Double = 4.0 in your Inputs. Such as:

    struct SliderView: View {
        @EnvironmentObject var inputs: Inputs
        
        var body: some View {
            HStack {
                Slider(value: $inputs.sampleNumber, in: 1...20.0, step: 1.0)
                Text("Sample \(Int(inputs.sampleNumber))")
            }
        }
    }
    
    class Inputs: ObservableObject {
        @Published var sampleNumber: Double = 4.0  // <-- here
    }