in order to customise a UISlider
, I use it in a UIViewRepresentable
. It exposes a @Binding var value: Double
so that my view model (ObservableObject
) view can observe the changes and update a View
accordingly.
The issue is that the view is not updated when the @Binding
value is changed. In the following example, I have two sliders. One native Slider
and one custom SwiftUISlider
.
Both pass a binding value to the view model that should update the view. The native Slider
does update the view but not the custom one. In the logs, I can see that the $sliderValue.sink { ...
is correctly called but the view is not updated.
I noticed this is happening when the presenting view has the @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
property. If I comment it out, it works as expected.
A complete sample code to reproduce this is
import SwiftUI
import Combine
struct ContentView: View {
@State var isPresentingModal = false
// comment this out
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Button("Show modal") {
isPresentingModal = true
}
.padding()
}
.sheet(isPresented: $isPresentingModal) {
MyModalView(viewModel: TempViewModel())
}
}
}
class TempViewModel: ObservableObject {
@Published var sliderText = ""
@Published var sliderValue: Double = 0
private var cancellable = Set<AnyCancellable>()
init() {
$sliderValue
.print("view model")
.sink { [weak self] value in
guard let self = self else { return }
print("updating view \(value)")
self.sliderText = "\(value) C = \(String(format: "%.2f" ,value * 9 / 5 + 32)) F"
}
.store(in: &cancellable)
}
}
struct MyModalView: View {
@ObservedObject var viewModel: TempViewModel
var body: some View {
VStack(alignment: .leading) {
Text("SwiftUI Slider")
Slider(value: $viewModel.sliderValue, in: -100...100, step: 0.5)
.padding(.bottom)
Text("UIViewRepresentable Slider")
SwiftUISlider(minValue: -100, maxValue: 100, value: $viewModel.sliderValue)
Text(viewModel.sliderText)
}
.padding()
}
}
struct SwiftUISlider: UIViewRepresentable {
final class Coordinator: NSObject {
var value: Binding<Double>
init(value: Binding<Double>) {
self.value = value
}
@objc func valueChanged(_ sender: UISlider) {
let index = Int(sender.value + 0.5)
sender.value = Float(index)
print("value changed \(sender.value)")
self.value.wrappedValue = Double(sender.value)
}
}
var minValue: Int = 0
var maxValue: Int = 0
@Binding var value: Double
func makeUIView(context: Context) -> UISlider {
let slider = UISlider(frame: .zero)
slider.minimumTrackTintColor = .systemRed
slider.maximumTrackTintColor = .systemRed
slider.maximumValue = Float(maxValue)
slider.minimumValue = Float(minValue)
slider.addTarget(
context.coordinator,
action: #selector(Coordinator.valueChanged(_:)),
for: .valueChanged
)
adapt(slider, context: context)
return slider
}
func updateUIView(_ uiView: UISlider, context: Context) {
adapt(uiView, context: context)
}
func makeCoordinator() -> SwiftUISlider.Coordinator {
Coordinator(value: $value)
}
private func adapt(_ slider: UISlider, context: Context) {
slider.value = Float(value)
}
}
struct PresentationMode_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}