Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I track when value changed AND when dragging stopped?

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.

like image 560
Lastmboy Avatar asked May 18 '18 21:05

Lastmboy


2 Answers

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.

like image 95
Nordeast Avatar answered Oct 20 '22 09:10

Nordeast


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.

like image 26
dinesharjani Avatar answered Oct 20 '22 09:10

dinesharjani