Swiftui: How to Get Continuous Updates from Slider

SwiftUI: How to get continuous updates from Slider

In SwiftUI, you can bind UI elements such as slider to properties in your data model and implement your business logic there.

For example, to get continuous slider updates:

import SwiftUI
import Combine

final class SliderData: BindableObject {

let didChange = PassthroughSubject<SliderData,Never>()

var sliderValue: Float = 0 {
willSet {
print(newValue)
didChange.send(self)
}
}
}

struct ContentView : View {

@EnvironmentObject var sliderData: SliderData

var body: some View {
Slider(value: $sliderData.sliderValue)
}
}

Note that to have your scene use the data model object, you need to update your window.rootViewController to something like below inside SceneDelegate class, otherwise the app crashes.

window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(SliderData()))

Sample Image

How to trigger a function when a Slider changes in SwiftUI?

You can make a custom Binding that calls the function in the Binding's setter:

@State var progress: Float = 0.5
var body: some View {

VStack{
Slider(value: Binding(get: {
self.progress
}, set: { (newVal) in
self.progress = newVal
self.sliderChanged()
}))
.padding(.all)
Text(String(progress))
}
}

func sliderChanged() {
print("Slider value changed to \(progress)")
}

Update text with Slider value in List from an array in SwiftUI

The @ObservableObject won't work within a struct like that -- it's only useful inside a SwiftUI View or a DynamicProperty. With your use case, because the class is a reference type, the @Published property has no way of knowing that the SliderVal was changed, so the owner View never gets updated.

You can fix this by turning your model into a struct:

struct Criteria: Identifiable {
var id = UUID()
var name: String
var slider_value: SliderVal = SliderVal()
static var count: Int = 0

init(name: String) {
self.name = name
}
}

struct SliderVal {
var value:Double = 50
}

The problem, once you do this, is you don't have a Binding to use in your List. If you're lucky enough to be on SwiftUI 3.0 (iOS 15 or macOS 12), you can use $criteria within your list to get a binding to the element being currently iterated over.

If you're on an earlier version, you'll need to either use indexes to iterate over the items, or, my favorite, create a custom binding that is tied to the id of the item. It looks like this:

struct ContentView: View {

@ObservedObject var db: Db = Db()

private func bindingForId(id: UUID) -> Binding<Criteria> {
.init {
db.criteria_db.first { $0.id == id } ?? Criteria(name: "")
} set: { newValue in
db.criteria_db = db.criteria_db.map {
$0.id == id ? newValue : $0
}
}
}

var body: some View {
NavigationView{
List(db.criteria_db){criteria in
VStack {
HStack{
Text(criteria.name).bold()
Spacer()
Text(String(criteria.slider_value.value))
}
Slider(value: bindingForId(id: criteria.id).slider_value.value, in:0...100, step: 1)
}
}
.navigationBarTitle("Criteria")
.navigationBarItems(trailing:
Button(action: {
Criteria.count += 1
db.criteria_db.append(Criteria(name: "Criteria\(Criteria.count)"))
dump(db.criteria_db)
}, label: {
Text("Add Criteria")
})
)
}
.listStyle(InsetGroupedListStyle())
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(db: Db())
}
}

class Db: ObservableObject {
@Published var criteria_db: [Criteria] = []
}

Now, because the models are all value types (structs), the View and @Published know when to update and your sliders work as expected.

How to decrease Slider ball size?

SwiftUI Slider doesn't provide API to customize the ‘thumb’.

UISlider has a setThumbImage(_:for:) that lets you customize the thumb's appearance. You could write your own UIViewRepresentable wrapper for UISlider. This is what I'd do.

You could also try using the SwiftUI-Introspect package to get access to the underlying UISlider to customize, but I haven't tried it so I don't know how well that works. It might also break in a future version of SwiftUI.

SwiftUI: Displaying slider position in a seperate label

Ok, I've worked out why my code wasn't updating as it should. It came down to my model which looks like this (Simple version):

final class Calculator: BindableObject {

let didChange = PassthroughSubject<Int, Never>()
var total: Int = 0
var clarity: Double = 0.0 { didSet { calculate() }}

private func calculate() {
if newValue.rounded() != Double(total) {
total = Int(newValue)
didChange.send(self.total)
}
}
}

What was happening was that the value on the sider was only updating when this model executed the didChange.send(self.total) line. I think, if I've got this right, that because the label is watching a binding, only when the binding updates does the label update. Makes sense. Still working this out.

I guess it part of learning about Combine and how it works :-)

iPhone : How to detect the end of slider drag?

If you don't need any data inbetween drag, than you should simply set:

[mySlider setContinuous: NO];

This way you will receive valueChanged event only when the user stops moving the slider.

Swift 5 version:

mySlider.isContinuous = false

Setting the value of a SwiftUI slider by tapping on it

Here's a minimal implementation I created using a GeometryReader that rounds to the closest value:

struct TappableSlider: View {
var value: Binding<Float>
var range: ClosedRange<Float>
var step: Float

var body: some View {
GeometryReader { geometry in
Slider(value: self.value, in: self.range, step: self.step)
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
let percent = min(max(0, Float(value.location.x / geometry.size.width * 1)), 1)
let newValue = self.range.lowerBound + round(percent * (self.range.upperBound - self.range.lowerBound))
self.value.wrappedValue = newValue
})
}
}
}

which can be used just like a normal slider, e.g.

TappableSlider(value: $sliderValue, in: 1...7, step: 1.0)


Related Topics



Leave a reply



Submit