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()))
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 (struct
s), 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
In-App Purchase Sandbox Environment Loop
How to Change an iOS Device Volume Programmatically
How to Use Phcachingimagemanager
Cancel Button Is Not Shown in Uisearchbar
Avplayerviewcontroller Using Audio-Only Avplayer
Autolayout: Uiview Within Uiview Has Incorrect Frame
Displaying Text One Character at a Time in Swift 2.0
Uitextfield Attributedplaceholder Has No Effect
Swift 3.0 Multiple Selection with Select All Cell
How to Play Multiple Audio Files Simultaneously
Changing Uipageviewcontroller's Page Programmatically Doesn't Update the Uipagecontrol
Xcode Creates Wrong IPA Folder Structure
Allow Full Access Check in Keyboards iOS10
Cannot Invoke 'Indexof' with an Argument List of Type '(Checklistitem)'
Uiapplicationlaunchoptionsremotenotificationkey Not Getting Userinfo
The Proper Way of Doing Chain Animations
Assertion Failure in Void _Uiperformresizeoftextviewfortextcontainer