How can I track when value changed AND when dragging stopped?
Asked Answered
C

2

5

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.

Christinachristine answered 18/5, 2018 at 21:20 Comment(9)
Your requirement is confusing. If isContinuous is true, there is no need to update anything when dragging stops because you would have already been told about the most recent update just before the dragging stopped.Levan
The value isn't the issue. How do I KNOW when dragging stopped?Christinachristine
This is the confusing part. Why do you need to know it stopped? You already have the latest value.Levan
The slider is a distance slider in km. I want to update a km label when the value changes on continuous. However I do NOT want to retrieve data from the server on every value change, as it could queue up thousands of data retrievals. I only want to fire the data retrieval with the last slider value (which I have) when the dragging actually stops. i.e. I only want to do the retrieval once, even though I am updating a label continuously. I realize that it's not entirely "proper", but that's the requirement.Christinachristine
What you are looking for is called debouncing. #47164121 You want to coalesce the call backs while the user is dragging the slider and only make the call after no more slides have happened for a period of time. Then you make the call to the server.Merrymaking
UISlider is a UIControl. I've never tried but you might be able to add a target/action for one or more of the "touch-up" events.Levan
@AllenR, debouncing looks interesting. I'll have to study up on this a bit and give it a try.Christinachristine
@maddy, I did get TouchUpInside to fire when you lift your finger off the slider. That would actually work. However, I may be wrong, but if I understand it correctly, the debouncing that AllenR suggests would actually be one step better, as it would allow me to track when the user "pauses" i.e. stopped dragging but finger still on slider. That would actually be perfect. I'll have to experiment a bit more.Christinachristine
@AllenR, debouncing works absolutely perfectly for what I need. Got it up and running great. Thanks. Post your answer, if you want.Christinachristine
M
7

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.

Merrymaking answered 21/5, 2018 at 14:41 Comment(0)
S
2

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.

Shaughnessy answered 4/4, 2019 at 9:25 Comment(2)
Thanks @dinesharjani. Yours is a great solution, too. The reason the debouncing worked so good for me is that it tracks whenever your finger pauses but is still on the slider. For most people, this wouldn't matter, but it works really good in my case. As they slide their finger across the slider, it doesn't update until they pause or lift their finger.Christinachristine
Funnily enough I had to use my debouncing code or else I put too much strain on the CPU ^^ Glad it worked for you @ChristinachristineShaughnessy

© 2022 - 2024 — McMap. All rights reserved.