'SwiftUI: How to get continuous updates from Slider
I'm experimenting with SwiftUI and the Slider control like this:
struct MyView: View {
@State private var value = 0.5
var body: some View {
Slider(value: $value) { pressed in
}
}
}
I'm trying to get continuous updates from the Slider as the user drags it, however it appears that it only updates the value at the end of the value change.
Anyone played with this? know how to get a SwiftUI Slider to issue a stream of value changes? Combine perhaps?
Solution 1:[1]
After much playing around I ended up with the following code. It's a little cut down to keep the answer short, but here goes. There was a couple of things I needed:
- To read value changes from the slider and round them to the nearest integer before setting an external binding.
- To set a localized hint value based on the integer.
struct AspectSlider: View {
// The first part of the hint text localization key.
private let hintKey: String
// An external integer binding to be set when the rounded value of the slider
changes to a different integer.
private let value: Binding<Int>
// A local binding that is used to track the value of the slider.
@State var sliderValue: Double = 0.0
init(value: Binding<Int>, hintKey: String) {
self.value = value
self.hintKey = hintKey
}
var body: some View {
VStack(alignment: .trailing) {
// The localized text hint built from the hint key and the rounded slider value.
Text(LocalizedStringKey("\(hintKey).\(self.value.value)"))
HStack {
Text(LocalizedStringKey(self.hintKey))
Slider(value: Binding<Double>(
getValue: { self.$sliderValue.value },
setValue: { self.sliderChanged(toValue: $0) }
),
through: 4.0) { if !$0 { self.slideEnded() } }
}
}
}
private func slideEnded() {
print("Moving slider to nearest whole value")
self.sliderValue = self.sliderValue.rounded()
}
private func sliderChanged(toValue value: Double) {
$sliderValue.value = value
let roundedValue = Int(value.rounded())
if roundedValue == self.value.value {
return
}
print("Updating value")
self.value.value = roundedValue
}
}
Solution 2:[2]
In Version 11.4.1 (11E503a) & Swift 5. I didn't reproduce it. By using Combine, I could get continuously update from slider changes.
class SliderData: ObservableObject {
@Published var sliderValue: Double = 0
...
}
struct ContentView: View {
@ObservedObject var slider = SliderData()
var body: some View {
VStack {
Slider(value: $slider.sliderValue)
Text(String(slider.sliderValue))
}
}
}
Solution 3:[3]
We can go without custom bindings, custom inits, ObservableObjects, PassthroughSubjects, @Published and other complications. Slider has .onChange(of: perform:) modifier which is perfect for this case.
This answer can be rewritten as follows:
struct AspectSlider2: View {
@Binding var value: Int
let hintKey: String
@State private var sliderValue: Double = 0.0
var body: some View {
VStack(alignment: .trailing) {
Text(LocalizedStringKey("\(hintKey)\(value)"))
HStack {
Slider(value: $sliderValue, in: 0...5)
.onChange(of: sliderValue, perform: sliderChanged)
}
}
}
private func sliderChanged(to newValue: Double) {
sliderValue = newValue.rounded()
let roundedValue = Int(sliderValue)
if roundedValue == value {
return
}
print("Updating value")
value = roundedValue
}
}
Solution 4:[4]
I am not able to reproduce this issue on iOS 13 Beta 2. Which operating system are you targeting?
Using a custom binding, the value is printed for every small change, not only after editing ended.
Slider(value: Binding<Double>(getValue: {0}, setValue: {print($0)}))
Note, that the closure ({ pressed in }) only reports when editing end starts and ends, the value stream is only passed into the binding.
Solution 5:[5]
What about like this:
(1) First you need the observable ...
import SwiftUI
import PlaygroundSupport
// make your observable double for the slider value:
class SliderValue: ObservableObject {
@Published var position: Double = 11.0
}
(2) When you make the slider, you have to PASS IN an instance of the observable:
So in HandySlider it is declared as an ObservedObject. (Don't forget, you're not "making" it there. Only declare it as a StateObject where you are "making" it.)
(3) AND you use the "$" for the Slider value as usual in a slider
(It seems the syntax is to use it on the "whole thing" like this "$sliderValue.position" rather than on the value per se, like "sliderValue.$position".)
struct HandySlider: View {
// don't forget to PASS IN a state object when you make a HandySlider
@ObservedObject var sliderValue: SliderValue
var body: some View {
HStack {
Text("0")
Slider(value: $sliderValue.position, in: 0...20)
Text("20")
}
}
}
(4) Actually make the state object somewhere.
(So, you use "StateObject" to do that, not "ObservedObject".)
And then
(5) use it freely where you want to display the value.
struct ContentView: View {
// here we literally make the state object
// (you'd just make it a "global" but not possible in playground)
@StateObject var sliderValue = SliderValue()
var body: some View {
HandySlider(sliderValue: sliderValue)
.frame(width: 400)
Text(String(sliderValue.position))
}
}
PlaygroundPage.current.setLiveView(ContentView())
Test it ...
Here's the whole thing to paste in a playground ...
import SwiftUI
import PlaygroundSupport
class SliderValue: ObservableObject {
@Published var position: Double = 11.0
}
struct HandySlider: View {
@ObservedObject var sliderValue: SliderValue
var body: some View {
HStack {
Text("0")
Slider(value: $sliderValue.position, in: 0...20)
Text("20")
}
}
}
struct ContentView: View {
@StateObject var sliderValue = SliderValue()
var body: some View {
HandySlider(sliderValue: sliderValue)
.frame(width: 400)
Text(String(sliderValue.position))
}
}
PlaygroundPage.current.setLiveView(ContentView())
Summary ...
You'll need an ObservableObject class: those contain Published variables.
Somewhere (obviously one place only) you will literally make that observable object class, and that's StateObject
Finally you can use that observable object class anywhere you want (as many places as needed), and that's ObservedObject
And in a slider ...
In the tricky case of a slider in particular, the desired syntax seems to be
Slider(value: $ooc.pitooc, in: 0...20)
ooc - your observable object class
pitooc - a property in that observable object class
You would not create the observable object class inside the slider, you create it elsewhere and pass it in to the slider. (So indeed in the slider class it is an observed object, not a state object.)
Solution 6:[6]
iOS 13.4, Swift 5.x
An answer based on Mohammid excellent solution, only I didn't want to use environmental variables.
class SliderData: ObservableObject {
let didChange = PassthroughSubject<SliderData,Never>()
@Published var sliderValue: Double = 0 {
didSet {
print("sliderValue \(sliderValue)")
didChange.send(self)
}
}
}
@ObservedObject var sliderData:SliderData
Slider(value: $sliderData.sliderValue, in: 0...Double(self.textColors.count))
With a small change to ContentView_Preview and the same in SceneDelegate.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(sliderData: SliderData.init())
}
}
Solution 7:[7]
If the value is in a navigation, child view:
Here's the case if the slider is, say, a popup which allows you to adjust a value.
It's actually simpler, nothing needs to be passed in to the slider. Just use an @EnvironmentObject.
Don't forget environment objects must be in the ancestor chain (you can't unfortunately go "sideways").
EnvironmentObject is only for parent-child chains.
Somewhat confusingly, you can't use the simple EnvironmentObject system if the items in question are in the same "environment!" EnvironmentObject should perhaps be named something like "ParentChainObject" or "NavigationViewChainObject".
EnvironmentObject is only used when you are using NavigationView.
import SwiftUI
import PlaygroundSupport
// using ancestor views ...
class SliderValue: ObservableObject {
@Published var position: Double = 11.0
}
struct HandySliderPopUp: View {
@EnvironmentObject var sv: SliderValue
var body: some View {
Slider(value: $sv.position, in: 0...10)
}
}
struct ContentView: View {
@StateObject var sliderValue = SliderValue()
var body: some View {
NavigationView {
VStack{
NavigationLink(destination:
HandySliderPopUp().frame(width: 400)) {
Text("click me")
}
Text(String(sliderValue.position))
}
}
.environmentObject(sliderValue) //HERE
}
}
PlaygroundPage.current.setLiveView(ContentView())
Note that //HERE is where you "set" the environment object.
For the "usual" situation, where it's the "same" view, see other answer.
Solution 8:[8]
Late to the party, this is what I did:
struct DoubleSlider: View {
@State var value: Double
let range: ClosedRange<Double>
let step: Double
let onChange: (Double) -> Void
init(initialValue: Double, range: ClosedRange<Double>, step: Double, onChange: @escaping (Double) -> Void) {
self.value = initialValue
self.range = range
self.step = step
self.onChange = onChange
}
var body: some View {
let binding = Binding<Double> {
return value
} set: { newValue in
value = newValue
onChange(newValue)
}
Slider(value: binding, in: range, step: step)
}
}
Usage:
DoubleSlider(initialValue: state.tipRate, range: 0...0.3, step: 0.01) { rate in
viewModel.state.tipRate = rate
}
Solution 9:[9]
Just use the onEditingChanged parameter of Slider. The argument is true while the user is moving the slider or still in contact with it. I do my updates when the argument changes from true to false.
struct MyView: View {
@State private var value = 0.5
func update(changing: Bool) -> Void {
// Do whatever
}
var body: some View {
Slider(value: $value, onEditingChanged: {changing in self.update(changing) })
{ pressed in }
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | drekka |
| Solution 2 | |
| Solution 3 | |
| Solution 4 | Palle |
| Solution 5 | |
| Solution 6 | user3069232 |
| Solution 7 | |
| Solution 8 | |
| Solution 9 |
