UIViewRepresentable not updating binding to ObservedObject property?
Asked Answered
B

2

7

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
        }
    }
}

Boudreau answered 8/12, 2020 at 23:39 Comment(4)
Slider accent color can be changed with .accentColor()Curiel
@Curiel I just tried it today in the latest version of Xcode on simulator and my iPad. It has no effect.Boudreau
Works fine for both with Xcode 12.1 / iOS 14.1, so probably a bug in your environment. Which one do you use?James
@James I'm on Xcode 12.2 (12B45b) and iOS 14.2Boudreau
T
3

@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()
    }
}
Tow answered 9/12, 2020 at 3:8 Comment(6)
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?Boudreau
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.Tow
Updated my question with an exampleBoudreau
Then uncomment the @Binding and remove the @EnvironmentObjectin 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.maxTow
Of course I jumped on stack overflow and see someone storing the View in a Coordinator... do not do that everMedicinal
@Medicinal here is the most prominent example maybe you should talk to Apple about it developer.apple.com/tutorials/swiftui/interfacing-with-uikitTow
C
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))
    }
    
}
Curiel answered 8/12, 2020 at 23:58 Comment(2)
MacOS 11 and latest versions of everything? If not must be a bugBoudreau
It does not seem to work with a @StateObject.Jaws

© 2022 - 2024 — McMap. All rights reserved.