4

This view is a UIKit slider adapted to my SwiftUI project because in SwiftUI Slider cannot change its track color, which is probably a bug since you should be able to change it with .accentColor.

Anyway, this slider changes its track color according to its value, from green to red. The whole thing works perfectly (although my gradients aren't that good yet) if value is bound to a normal @State property, but the second you try to attach it to a property of an @ObservedObject it breaks and, although the track color still works, it never changes the underlying value. I would like to think that this is just a bug right now but it's more likely there's something here that needs to be fixed.

struct RedscaleSlider: UIViewRepresentable {
    
    @Binding var value: Double
    
    var min: Double
    var max: Double
    
    class Coordinator: NSObject {
        @Binding var value: Double
        var min: Double
        var max: Double
        
        init(value: Binding<Double>, min: Double = 0, max: Double = 100) {
           _value = value
            self.min = min
            self.max = max
        }
        
        @objc func valueChanged(_ sender: UISlider) {
            self.value = Double(sender.value)
            sender.minimumTrackTintColor = green_to_red_gradient(value: (Double(sender.value) - min) / (max - min)).into_UIKit_color()
        }
    }
    
    var thumb_color: UIColor = .white
    var track_color: UIColor = .systemBlue
    
    func makeUIView(context: Context) -> UISlider {
        let slider = UISlider(frame: .zero)
        
        slider.addTarget(
            context.coordinator,
            action: #selector(Coordinator.valueChanged),
            for: .valueChanged
        )
        
        slider.thumbTintColor = thumb_color
        slider.minimumTrackTintColor = track_color
        slider.minimumValue = Float(min)
        slider.maximumValue = Float(max)
        slider.value = Float(value)
        
        return slider
    }
    
    func updateUIView(_ UI_View: UISlider, context: Context) {
        UI_View.value = Float(self.value)
    }
    
    func makeCoordinator() -> RedscaleSlider.Coordinator {
        Coordinator(value: $value, min: self.min, max: self.max)
    }
}

EDIT: Example of how it should be able to be used:

class ViewModel: ObservableObject {
    @Published var danger_level: Double
}
struct ExampleView: View {
    @ObservedObject var view_model = ViewModel(danger_level: 50)
    
    var body: some View {
        VStack {
            Text(view_model.danger_level.description)
            RedscaleSlider(value: $view_model.danger_level)
            // should update view model just like a Stepper would
        }
    }
}

XY L
  • 25,431
  • 14
  • 84
  • 143
TigerGold
  • 163
  • 1
  • 7

2 Answers2

2

@Binding is only supposed to be used in a View/UIViewRepresentable

Instead of having an @Binding in the Coordinator switch the init to receive the init(_ parent: RedscaleSlider) then use parent.value = Double(sender.value)

import SwiftUI

class RedscaleSliderViewModel : ObservableObject {
    @Published var value : Double = 5
    @Published var danger_level: Double = 7.5

}
struct ParentRedscaleSlider: View{
    //@State var value: Double = 5
    @StateObject var vm = RedscaleSliderViewModel()
    var body: some View {
        VStack{
            Text(vm.danger_level.description)
            RedscaleSlider(value: $vm.danger_level, min: 0, max: 10)
            
        }
    }
}
struct RedscaleSlider: UIViewRepresentable {
    //@EnvironmentObject var vm: RedscaleSliderViewModel
    @Binding var value: Double
    
    var min: Double
    var max: Double
    
    class Coordinator: NSObject {
        var parent: RedscaleSlider
        
        init(_ parent: RedscaleSlider) {
            self.parent = parent
        }
        
        @objc func valueChanged(_ sender: UISlider) {
            let senderVal = Double(sender.value)
            self.parent.value = senderVal
            
            //Missing code
            //sender.minimumTrackTintColor = green_to_red_gradient(value: (Double(sender.value) - min) / (max - min)).into_UIKit_color()
        }
    }
    
    var thumb_color: UIColor = .white
    var track_color: UIColor = .systemBlue
    
    func makeUIView(context: Context) -> UISlider {
        let slider = UISlider(frame: .zero)
        
        slider.addTarget(
            context.coordinator,
            action: #selector(Coordinator.valueChanged),
            for: .valueChanged
        )
        
        slider.thumbTintColor = thumb_color
        slider.minimumTrackTintColor = track_color
        slider.minimumValue = Float(min)
        slider.maximumValue = Float(max)
        slider.value = Float(value)
        
        return slider
    }
    
    func updateUIView(_ UI_View: UISlider, context: Context) {
        UI_View.value = Float(self.value)
    }
    
    func makeCoordinator() -> RedscaleSlider.Coordinator {
        Coordinator(self)
    }
}

struct RedScaleSlider_Previews: PreviewProvider {
    static var previews: some View {
        ParentRedscaleSlider()
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
  • Well one of the main points of this is to be able to use RedscaleSlider in multiple places, like a TextField view, to control lots of different values. This solution is inelegant at best for that. Is there any other way? – TigerGold Dec 10 '20 at 05:46
  • I don’t understand what you are trying to say with inelegant? How were you trying to setup the ObservedObject before? Are you trying ro use it as a a selector? What is your use case? Maybe a complete minimal example would get you a better answer. – lorem ipsum Dec 10 '20 at 11:59
  • Updated my question with an example – TigerGold Dec 10 '20 at 19:28
  • Then uncomment the `@Binding` and remove the `@EnvironmentObject`in the `UIViewRepresentable`. `@State` and `@ObservableObject` are used for storage (sources of truth). `@Binding` and `@EnvironmentObject` are two-way connectors. (I updated the code). Also, notice how inheriting the `parent` can also remove the initialization of `min` and `max` in the `Coordinator` cleaner code just invoke `parent.min` or `parent.max` – lorem ipsum Dec 10 '20 at 21:47
0

accentColor is working fine for me like that..

Also your code works fine with a ObservableObject. Here is the demo code:

class ViewModel : ObservableObject {
    @Published var double : Double = 0.0
}
struct ContentView : View {
    @State private var value: Double = 0
    
    @ObservedObject var viewModel = ViewModel()

    var body : some View {
        Slider(value: $value, in: -100...100, step: 0.1)
            .accentColor(.red) //<< here accent color
        
        RedscaleSlider(value: $viewModel.double, min: 5.0, max: 250.0)
        Text(String(viewModel.double))
    }
    
}
davidev
  • 7,694
  • 5
  • 21
  • 56