如何跟踪值何时更改以及拖动何时停止?

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

根据我使用 UISlider 的经验,我为 ValueChanged 事件创建了 IBAction。现在,根据我设置 isContinuous 的方式,它会在值更改或拖动停止时触发。我的问题是我需要跟踪两种情况,但我不能 isContinuous 设置两种方式。

有什么方法可以同时跟踪更改的值和用户停止拖动的时间?我想在值连续变化时更新计数器,而且,我想在拖动停止时刷新数据。我不想在每次值更改时都刷新数据,因为这会导致过多的开销。我尝试了其他几个操作,但其中 none 个在拖动停止时被调用。

您要查找的解决方案的术语称为 "Debouncing"。这个想法是,您合并对同一方法的频繁调用,并且仅在调用停止一段时间后才执行该方法。当您快速接收大量用户输入并且必须对该输入执行相对繁重的工作负载时,去抖动是改善用户体验的好方法。只有在用户完成输入后才执行工作可以避免 cpu 做太多工作并减慢应用程序的速度。一些示例可能是在用户滚动页面时移动视图,在用户输入搜索词时更新 table 视图,或者在点击一系列按钮后进行网络调用。

下面在 playground 中显示了一个示例,展示了如何使用 UISlider 实现它。您可以将示例复制并粘贴到空的 playground 中试一试。

//: 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

这个例子的核心是这个函数:

func debounce(seconds: TimeInterval, function: @escaping () -> Swift.Void ) {
    timer?.invalidate()
    timer = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false, block: { _ in
        function()
    })
}

这里每次调用 debounce 都会使计时器无效,然后安排一个新的计时器来调用传入的 function。这确保 function 不会被调用直到足够time has elapsed to not have timer 无效。

非常支持;它在每个级别都是正确的,不仅提供了令人满意的答案,而且超出了范围,教导任何在开发 iOS 应用程序时没有遇到或想过将事件合并为单一通知形式的人。

对于那些像我一样在一个地方需要一个简单的解决方案,并且不想陷入混乱的 Timer(s) 的人,这是我想出的解决方案在 用户从 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 { [=10=].state == .changed }
        guard activeRecognisers?.isEmpty ?? true else { return }

        // Code to execute when the user finishes changing the UISlider's value.
    }
}

解释:只要用户拖动 UISlider,就会有一个 UIGestureRecognizer 触发我们的目标。因此,对于每次更改,我们都会将稍后执行的闭包排入队列,并且每次都检查滑块中是否有任何活动的识别器。如果没有,则拖动手势已完成,我们可以安全地假设用户已抬起手指。