In my experience with UISlider, I create the IBAction for the ValueChanged
event. Now, depending on how I have isContinuous
set, it either fires when the value changes or when dragging stops. My problem is that I need to track BOTH scenarios, but I can't have isContinuous
set both ways.
Is there any way I can track both the value changed and when the user stops dragging? I want to update a counter when the value changes on continuous, and, as well, I want to refresh the data when dragging stops. I don't want to refresh the data on every value change, as it causes too much overhead. I tried several of the other actions, but none of them get called when dragging stops.
The term for the solution you are trying to find is called "Debouncing". The idea is that you coalesce frequent calls to the same method and only execute the method once the calls have stopped for a period of time. Debouncing is a great way to improve the user experience when you are taking in a lot of user input quickly and must do a relatively heavy workload on that input. Only executing the work when the user has completed their input saves the cpu from doing too much work and slowing the app down. Some examples might be moving a view when the user scrolls the page, updating a table view when the user enters a search term or making network calls after a series of button taps.
An example showing how one may implemented it with a UISlider
is shown below in a playground. You can copy and paste the example into an empty playground to give it a try.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
// Timer to record the length of time between debounced calls
var timer: Timer? = nil
override func loadView() {
super.loadView()
let view = UIView()
view.backgroundColor = .white
// Set up the slider
let slider = UISlider(frame: CGRect(x: 100, y: 100, width: 200, height: 50))
slider.minimumValue = 0
slider.maximumValue = 100
slider.isContinuous = true
slider.addTarget(self, action: #selector(sliderValueDidChange(_:)), for: .valueChanged)
self.view.addSubview(slider)
}
@objc func sliderValueDidChange(_ sender: UISlider) {
// Coalesce the calls until the slider valude has not changed for 0.2 seconds
debounce(seconds: 0.2) {
print("slider value: \(sender.value)")
}
}
// Debounce function taking a time interval to wait before firing after user input has stopped
// and a function to execute when debounce has stopped being called for a period of time.
func debounce(seconds: TimeInterval, function: @escaping () -> Swift.Void ) {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false, block: { _ in
function()
})
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
PlaygroundPage.current.needsIndefiniteExecution = true
The meat of the example is this function:
func debounce(seconds: TimeInterval, function: @escaping () -> Swift.Void ) {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false, block: { _ in
function()
})
}
Here every time debounce
is called it invalidates a timer and then schedules a new timer to call the function
passed in. This ensures that the function
is not called until enough time has elapsed to not have the timer invalidated.
I highly support Allen R's accepted answer; it is correct at every level and not only provides a satisfactory answer, but also goes above and beyond the scope, teaching whomever hasn't encountered or thought about coalescing events into a single form of notification while developing iOS apps.
For those of you who, like me, needed a simple solution in one spot, and don't want to get in the mess of playing with Timer
(s), here's the solution I came up with to execute some code after the user lifts their finger from a UISlider
:
@objc private func rssiSliderChanged(sender: UISlider?) {
// Value has changed.
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
let activeRecognisers = sender?.gestureRecognizers?.filter { $0.state == .changed }
guard activeRecognisers?.isEmpty ?? true else { return }
// Code to execute when the user finishes changing the UISlider's value.
}
}
Explanation: there's a UIGestureRecognizer
firing our target whenever the user drags the UISlider
. So, for every change, we enqueue a closure that'll be executed later, and every time check whether there's any active recognizer in the slider. If there aren't, then the dragging gesture has finished, and we can safely assume the user las lifted their finger.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With