【问题标题】:How can I debounce a method call?如何消除方法调用的抖动?
【发布时间】:2015-01-22 20:44:56
【问题描述】:

我正在尝试使用 UISearchView 来查询 Google 地方。这样做时,在我的UISearchBar 的文本更改呼叫中,我正在向谷歌地方发出请求。问题是我宁愿去抖动这个调用,每 250 毫秒只请求一次,以避免不必要的网络流量。我不想自己写这个功能,但如果需要的话我会写的。

我找到了:https://gist.github.com/ShamylZakariya/54ee03228d955f458389,但我不太清楚如何使用它:

func debounce( delay:NSTimeInterval, #queue:dispatch_queue_t, action: (()->()) ) -> ()->() {

    var lastFireTime:dispatch_time_t = 0
    let dispatchDelay = Int64(delay * Double(NSEC_PER_SEC))

    return {
        lastFireTime = dispatch_time(DISPATCH_TIME_NOW,0)
        dispatch_after(
            dispatch_time(
                DISPATCH_TIME_NOW,
                dispatchDelay
            ),
            queue) {
                let now = dispatch_time(DISPATCH_TIME_NOW,0)
                let when = dispatch_time(lastFireTime, dispatchDelay)
                if now >= when {
                    action()
                }
            }
    }
}

这是我使用上述代码尝试过的一件事:

let searchDebounceInterval: NSTimeInterval = NSTimeInterval(0.25)

func findPlaces() {
    // ...
}

func searchBar(searchBar: UISearchBar!, textDidChange searchText: String!) {
    debounce(
        searchDebounceInterval,
        dispatch_get_main_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT),
        self.findPlaces
    )
}

产生的错误是Cannot invoke function with an argument list of type '(NSTimeInterval, $T5, () -> ())

我该如何使用这种方法,或者在 iOS/Swift 中是否有更好的方法。

【问题讨论】:

  • 您是否尝试过使用链接库?
  • @nhgrif 我不确定那是什么,我对原生 iOS 领域还很陌生。
  • 什么是什么?您链接到 github 页面。我没看。您是否尝试使用那里可用的东西?为什么要关联?
  • @nhgrif 哎呀被误解了。我不太确定该函数的第二个参数是什么或如何使用它。浏览代码似乎是有道理的。我尝试了一些变体,但没有成功。
  • 您应该发布您从该代码中尝试过的内容。你可能已经接近了。

标签: ios swift throttling


【解决方案1】:

对于那些不想创建类/扩展的人来说,这是一个选项:

代码中的某处:

var debounce_timer:Timer?

在你想要去抖动的地方:

debounce_timer?.invalidate()
debounce_timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in 
    print ("Debounce this...") 
}

【讨论】:

  • 本页最简单的解决方案,为什么比这更复杂?
  • 这是正确答案。如果人们为一个非常简单的问题提供 2 页的答案,您应该始终持怀疑态度。
  • 这可能可行,但由于这将安排在默认运行循环上,因此除非您明确添加计时器,否则这将不适用于 macOS 上的其他运行循环(例如显示对话框时)到NSModalPanelRunLoopMode 或事件跟踪运行循环。
【解决方案2】:

如果您想保持整洁,这里有一个基于 GCD 的解决方案,它可以使用熟悉的基于 GCD 的语法来完成您需要的工作: https://gist.github.com/staminajim/b5e89c6611eef81910502db2a01f1a83

DispatchQueue.main.asyncDeduped(target: self, after: 0.25) { [weak self] in
     self?.findPlaces()
}

findPlaces() 只会在最后一次调用 asyncDuped 后 0.25 秒后调用一次

【讨论】:

  • 这真的很聪明@staminajim,很好!
【解决方案3】:

Swift 3 版本

1。基本去抖功能

func debounce(interval: Int, queue: DispatchQueue, action: @escaping (() -> Void)) -> () -> Void {
    var lastFireTime = DispatchTime.now()
    let dispatchDelay = DispatchTimeInterval.milliseconds(interval)

    return {
        lastFireTime = DispatchTime.now()
        let dispatchTime: DispatchTime = DispatchTime.now() + dispatchDelay

        queue.asyncAfter(deadline: dispatchTime) {
            let when: DispatchTime = lastFireTime + dispatchDelay
            let now = DispatchTime.now()
            if now.rawValue >= when.rawValue {
                action()
            }
        }
    }
}

2。参数化去抖函数

有时让 debounce 函数带一个参数很有用。

typealias Debounce<T> = (_ : T) -> Void

func debounce<T>(interval: Int, queue: DispatchQueue, action: @escaping Debounce<T>) -> Debounce<T> {
    var lastFireTime = DispatchTime.now()
    let dispatchDelay = DispatchTimeInterval.milliseconds(interval)

    return { param in
        lastFireTime = DispatchTime.now()
        let dispatchTime: DispatchTime = DispatchTime.now() + dispatchDelay

        queue.asyncAfter(deadline: dispatchTime) {
            let when: DispatchTime = lastFireTime + dispatchDelay
            let now = DispatchTime.now()

            if now.rawValue >= when.rawValue {
                action(param)
            }
        }
    }
}

3。示例

在以下示例中,您可以看到去抖是如何工作的,使用字符串参数来识别调用。

let debouncedFunction = debounce(interval: 200, queue: DispatchQueue.main, action: { (identifier: String) in
    print("called: \(identifier)")
})

DispatchQueue.global(qos: .background).async {
    debouncedFunction("1")
    usleep(100 * 1000)
    debouncedFunction("2")
    usleep(100 * 1000)
    debouncedFunction("3")
    usleep(100 * 1000)
    debouncedFunction("4")
    usleep(300 * 1000) // waiting a bit longer than the interval
    debouncedFunction("5")
    usleep(100 * 1000)
    debouncedFunction("6")
    usleep(100 * 1000)
    debouncedFunction("7")
    usleep(300 * 1000) // waiting a bit longer than the interval
    debouncedFunction("8")
    usleep(100 * 1000)
    debouncedFunction("9")
    usleep(100 * 1000)
    debouncedFunction("10")
    usleep(100 * 1000)
    debouncedFunction("11")
    usleep(100 * 1000)
    debouncedFunction("12")
}

注意:usleep() 函数仅用于演示目的,可能不是真正的应用程序最优雅的解决方案。

结果

当自上次调用后至少有 200 毫秒的间隔时,您总是会收到回调。

调用:4
叫:7
叫:12

【讨论】:

  • 现在看起来好多了。
  • 奇怪,但这不起作用。当我以短间隔在字段中输入字母时,我什至没有完成所有文本输入时得到这样的输出:1, 12, 123, 1234, 12345, 123456, 1234567, 12345678, 123456789, 1234567890 函数间隔是 2000 毫秒,所以我输入字母太慢不是问题。
  • 这不起作用,我在Search Result Updater 上使用了它。所有的进程确实都被延迟了,但是经过一段时间后,所有的进程都被执行了。与预期不符(预期:仅执行最后一项工作)
【解决方案4】:

尽管这里有几个很好的答案,但我想我会分享我最喜欢的(pure Swift)方法来消除用户输入的搜索...

1) 添加这个简单的类(Debounce.swift):

import Dispatch

class Debounce<T: Equatable> {

    private init() {}

    static func input(_ input: T,
                      comparedAgainst current: @escaping @autoclosure () -> (T),
                      perform: @escaping (T) -> ()) {

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            if input == current() { perform(input) }
        }
    }
}

2) 可选择包含此单元测试 (DebounceTests.swift):

import XCTest

class DebounceTests: XCTestCase {

    func test_entering_text_delays_processing_until_settled() {
        let expect = expectation(description: "processing completed")
        var finalString: String = ""
        var timesCalled: Int = 0
        let process: (String) -> () = {
            finalString = $0
            timesCalled += 1
            expect.fulfill()
        }

        Debounce<String>.input("A", comparedAgainst: "AB", perform: process)
        Debounce<String>.input("AB", comparedAgainst: "ABCD", perform: process)
        Debounce<String>.input("ABCD", comparedAgainst: "ABC", perform: process)
        Debounce<String>.input("ABC", comparedAgainst: "ABC", perform: process)

        wait(for: [expect], timeout: 2.0)

        XCTAssertEqual(finalString, "ABC")
        XCTAssertEqual(timesCalled, 1)
    }
}

3) 在您想延迟处理的任何地方使用它(例如 UISearchBarDelegate):

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    Debounce<String>.input(searchText, comparedAgainst: searchBar.text ?? "") {
        self.filterResults($0)
    }
}

基本前提是我们只是将输入文本的处理延迟了 0.5 秒。那时,我们将从事件中得到的字符串与搜索栏的当前值进行比较。如果它们匹配,我们假设用户已经暂停输入文本,我们继续进行过滤操作。

由于它是通用的,它适用于任何类型的等价值。

由于 Dispatch 模块自版本 3 起已包含在 Swift 核心库中,这个类也可以安全地用于非 Apple 平台

【讨论】:

  • 不错,简洁,解决方案。
  • 这个解决方案的好处是它可以是静态的,即它不需要维护状态来执行逻辑,而是将责任委托给调用者。
  • 我从这个解决方案中学到了一些关于 Swift 的新东西,非常感谢!
【解决方案5】:

把它放在你文件的顶层,这样你就不会被 Swift 有趣的参数命名规则弄糊涂了。请注意,我已删除 #,因此现在所有参数都没有名称:

func debounce( delay:NSTimeInterval, queue:dispatch_queue_t, action: (()->()) ) -> ()->() {
    var lastFireTime:dispatch_time_t = 0
    let dispatchDelay = Int64(delay * Double(NSEC_PER_SEC))

    return {
        lastFireTime = dispatch_time(DISPATCH_TIME_NOW,0)
        dispatch_after(
            dispatch_time(
                DISPATCH_TIME_NOW,
                dispatchDelay
            ),
            queue) {
                let now = dispatch_time(DISPATCH_TIME_NOW,0)
                let when = dispatch_time(lastFireTime, dispatchDelay)
                if now >= when {
                    action()
                }
        }
    }
}

现在,在您的实际课程中,您的代码将如下所示:

let searchDebounceInterval: NSTimeInterval = NSTimeInterval(0.25)
let q = dispatch_get_main_queue()
func findPlaces() {
    // ...
}
let debouncedFindPlaces = debounce(
        searchDebounceInterval,
        q,
        findPlaces
    )

现在debouncedFindPlaces 是一个你可以调用的函数,并且你的findPlaces 不会被执行,除非你上次调用它之后delay 已经过去了。

【讨论】:

  • 我不知道如何在课堂上使用它?我无法在我的 updateSearchResultsForSearchController 方法中创建去抖函数,因为它会一次又一次地重新创建该函数。如何创建去抖动方法?我是 Swift 的新手,也许我遗漏了一些东西。
  • 您可以在 owenoak 的回答中找到类似的示例。 TLDR:使用这样的东西:lazy var debouncedFindPlaces: ()-&gt;() = debounce( NSTimeInterval(0.25), dispatch_get_main_queue(), self.findPlaces )
  • 这在 Swift4 或 5 中不起作用。dispatch_queue_t 已被删除且未被替换。取而代之的是 DispatchQueue 的东西,它的运作方式完全不同。
  • @Shayne 实际上不,完全一样。名称和 OO 结构已经更新,但底层 GCD 没有改变,这个古老的答案可以直接逐字翻译成 Swift 5。
【解决方案6】:

首先,创建一个 Debouncer 泛型类:

//
//  Debouncer.swift
//
//  Created by Frédéric Adda

import UIKit
import Foundation

class Debouncer {

    // MARK: - Properties
    private let queue = DispatchQueue.main
    private var workItem = DispatchWorkItem(block: {})
    private var interval: TimeInterval

    // MARK: - Initializer
    init(seconds: TimeInterval) {
        self.interval = seconds
    }

    // MARK: - Debouncing function
    func debounce(action: @escaping (() -> Void)) {
        workItem.cancel()
        workItem = DispatchWorkItem(block: { action() })
        queue.asyncAfter(deadline: .now() + interval, execute: workItem)
    }
}

然后创建一个使用去抖动机制的 UISearchBar 的子类:

//
//  DebounceSearchBar.swift
//
//  Created by Frédéric ADDA on 28/06/2018.
//

import UIKit

/// Subclass of UISearchBar with a debouncer on text edit
class DebounceSearchBar: UISearchBar, UISearchBarDelegate {

    // MARK: - Properties

    /// Debounce engine
    private var debouncer: Debouncer?

    /// Debounce interval
    var debounceInterval: TimeInterval = 0 {
        didSet {
            guard debounceInterval > 0 else {
                self.debouncer = nil
                return
            }
            self.debouncer = Debouncer(seconds: debounceInterval)
        }
    }

    /// Event received when the search textField began editing
    var onSearchTextDidBeginEditing: (() -> Void)?

    /// Event received when the search textField content changes
    var onSearchTextUpdate: ((String) -> Void)?

    /// Event received when the search button is clicked
    var onSearchClicked: (() -> Void)?

    /// Event received when cancel is pressed
    var onCancel: (() -> Void)?

    // MARK: - Initializers
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        delegate = self
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        delegate = self
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        delegate = self
    }

    // MARK: - UISearchBarDelegate
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        onCancel?()
    }

    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        onSearchClicked?()
    }

    func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
        onSearchTextDidBeginEditing?()
    }

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        guard let debouncer = self.debouncer else {
            onSearchTextUpdate?(searchText)
            return
        }
        debouncer.debounce {
            DispatchQueue.main.async {
                self.onSearchTextUpdate?(self.text ?? "")
            }
        }
    }
}

请注意,该类设置为 UISearchBarDelegate。动作将作为闭包传递给此类。

最后,你可以这样使用它:

class MyViewController: UIViewController {

    // Create the searchBar as a DebounceSearchBar
    // in code or as an IBOutlet
    private var searchBar: DebounceSearchBar?


    override func viewDidLoad() {
        super.viewDidLoad()

        self.searchBar = createSearchBar()
    }

    private func createSearchBar() -> DebounceSearchBar {
        let searchFrame = CGRect(x: 0, y: 0, width: 375, height: 44)
        let searchBar = DebounceSearchBar(frame: searchFrame)
        searchBar.debounceInterval = 0.5
        searchBar.onSearchTextUpdate = { [weak self] searchText in
            // call a function to look for contacts, like:
            // searchContacts(with: searchText)
        }
        searchBar.placeholder = "Enter name or email"
        return searchBar
    }
}

请注意,在这种情况下,DebounceSearchBar 已经是 searchBar 委托。您应该将此 UIViewController 子类设置为 searchBar 委托!也不使用委托函数。 请改用提供的闭包!

【讨论】:

    【解决方案7】:

    以下内容对我有用:

    将以下内容添加到您项目中的某个文件中(我为此类事情维护了一个“SwiftExtensions.swift”文件):

    // Encapsulate a callback in a way that we can use it with NSTimer.
    class Callback {
        let handler:()->()
        init(_ handler:()->()) {
            self.handler = handler
        }
        @objc func go() {
            handler()
        }
    }
    
    // Return a function which debounces a callback, 
    // to be called at most once within `delay` seconds.
    // If called again within that time, cancels the original call and reschedules.
    func debounce(delay:NSTimeInterval, action:()->()) -> ()->() {
        let callback = Callback(action)
        var timer: NSTimer?
        return {
            // if calling again, invalidate the last timer
            if let timer = timer {
                timer.invalidate()
            }
            timer = NSTimer(timeInterval: delay, target: callback, selector: "go", userInfo: nil, repeats: false)
            NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSDefaultRunLoopMode)
        }
    }
    

    然后在你的类中设置它:

    class SomeClass {
        ...
        // set up the debounced save method
        private var lazy debouncedSave: () -> () = debounce(1, self.save)
        private func save() {
            // ... actual save code here ...
        }
        ...
        func doSomething() {
            ...
            debouncedSave()
        }
    }
    

    您现在可以反复拨打someClass.doSomething(),每秒只会保存一次。

    【讨论】:

      【解决方案8】:

      我使用了这个受 Objective-C 启发的好方法:

      override func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
          // Debounce: wait until the user stops typing to send search requests      
          NSObject.cancelPreviousPerformRequests(withTarget: self) 
          perform(#selector(updateSearch(with:)), with: searchText, afterDelay: 0.5)
      }
      

      注意被调用的方法updateSearch一定要标注@objc!

      @objc private func updateSearch(with text: String) {
          // Do stuff here   
      }
      

      这种方法最大的优点是我可以传递参数(这里是搜索字符串)。此处介绍的大多数去抖动器都不是这种情况...

      【讨论】:

        【解决方案9】:

        该问题提供并建立在几个答案中的通用解决方案存在逻辑错误,导致去抖阈值较短。

        从提供的实现开始:

        typealias Debounce<T> = (T) -> Void
        
        func debounce<T>(interval: Int, queue: DispatchQueue, action: @escaping (T) -> Void) -> Debounce<T> {
            var lastFireTime = DispatchTime.now()
            let dispatchDelay = DispatchTimeInterval.milliseconds(interval)
        
            return { param in
                lastFireTime = DispatchTime.now()
                let dispatchTime: DispatchTime = DispatchTime.now() + dispatchDelay
        
                queue.asyncAfter(deadline: dispatchTime) {
                    let when: DispatchTime = lastFireTime + dispatchDelay
                    let now = DispatchTime.now()
        
                    if now.rawValue >= when.rawValue {
                        action(param)
                    }
                }
            }
        }
        

        以 30 毫秒的间隔进行测试,我们可以创建一个相对简单的示例来展示其弱点。

        let oldDebouncerDebouncedFunction = debounce(interval: 30, queue: .main, action: exampleFunction)
        
        DispatchQueue.global(qos: .background).async {
        
            oldDebouncerDebouncedFunction("1")
            oldDebouncerDebouncedFunction("2")
            sleep(.seconds(2))
            oldDebouncerDebouncedFunction("3")
        }
        

        这会打印出来

        调用:1
        叫:2
        叫:3

        这显然是不正确的,因为第一次调用应该被去抖动。使用更长的去抖动阈值(例如 300 毫秒)将解决问题。问题的根源在于错误地期望DispatchTime.now() 的值将等于传递给asyncAfter(deadline: DispatchTime)deadline。比较now.rawValue &gt;= when.rawValue 的目的是实际将预期截止日期与“最近”截止日期进行比较。在较小的去抖动阈值下,asyncAfter 的延迟成为一个需要考虑的非常重要的问题。

        虽然它很容易修复,并且代码可以在它之上变得更加简洁。通过仔细选择何时致电.now(),并确保将实际截止日期与最近安排的截止日期进行比较,我得出了这个解决方案。这对于threshold 的所有值都是正确的。请特别注意 #1 和 #2,因为它们在语法上是相同的,但如果在分派工作之前进行了多次调用,则会有所不同。

        typealias DebouncedFunction<T> = (T) -> Void
        
        func makeDebouncedFunction<T>(threshold: DispatchTimeInterval = .milliseconds(30), queue: DispatchQueue = .main, action: @escaping (T) -> Void) -> DebouncedFunction<T> {
        
            // Debounced function's state, initial value doesn't matter
            // By declaring it outside of the returned function, it becomes state that persists across
            // calls to the returned function
            var lastCallTime: DispatchTime = .distantFuture
        
            return { param in
        
                lastCallTime = .now()
                let scheduledDeadline = lastCallTime + threshold // 1
        
                queue.asyncAfter(deadline: scheduledDeadline) {
                    let latestDeadline = lastCallTime + threshold // 2
        
                    // If there have been no other calls, these will be equal
                    if scheduledDeadline == latestDeadline {
                        action(param)
                    }
                }
            }
        }
        

        实用程序

        func exampleFunction(identifier: String) {
            print("called: \(identifier)")
        }
        
        func sleep(_ dispatchTimeInterval: DispatchTimeInterval) {
            switch dispatchTimeInterval {
            case .seconds(let seconds):
                Foundation.sleep(UInt32(seconds))
            case .milliseconds(let milliseconds):
                usleep(useconds_t(milliseconds * 1000))
            case .microseconds(let microseconds):
                usleep(useconds_t(microseconds))
            case .nanoseconds(let nanoseconds):
                let (sec, nsec) = nanoseconds.quotientAndRemainder(dividingBy: 1_000_000_000)
                var timeSpec = timespec(tv_sec: sec, tv_nsec: nsec)
                withUnsafePointer(to: &timeSpec) {
                    _ = nanosleep($0, nil)
                }
            case .never:
                return
            }
        }
        

        希望这个答案能帮助其他人在函数柯里化解决方案中遇到意外行为。

        【讨论】:

          【解决方案10】:

          这里你有完全 Swift 5 友好和流畅的解决方案??

          你可以在检测tableView滚动到底部时使用它。

          NSObject.cancelPreviousPerformRequests(withTarget: self, 
                                                 selector: #selector(didScrollToBottom), 
                                                 object: nil)
          perform(#selector(didScrollToBottom), with: nil, afterDelay: TimeInterval(0.1))
          
          @objc private func didScrollToBottom() {
                print("finally called once!")
          }
          

          【讨论】:

          • 我很确定这实际上并没有取消之前的请求。编辑:nvm!超时时间非常短。将它扩展到 .4s 对我有用 :)
          【解决方案11】:

          quickthymeexcellent answer 的一些细微改进:

          1. 添加delay 参数,可能带有默认值。
          2. Debounce 设为enum 而不是class,这样您就不必声明private init
          enum Debounce<T: Equatable> {
              static func input(_ input: T, delay: TimeInterval = 0.3, current: @escaping @autoclosure () -> T, perform: @escaping (T) -> Void) {
                  DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
                      guard input == current() else { return }
                      perform(input)
                  }
              }
          }
          

          也没有必要在调用站点显式声明泛型类型——它可以被推断出来。例如,如果您想在updateSearchResults(for:) 中使用DebounceUISearchControllerUISearchResultsUpdating 的必需方法),您可以这样做:

          func updateSearchResults(for searchController: UISearchController) {
              guard let text = searchController.searchBar.text else { return }
          
              Debounce.input(text, current: searchController.searchBar.text ?? "") {
                  // ...
              }
          
          }
          

          【讨论】:

            【解决方案12】:

            这是 Swift 3 的去抖动实现。

            https://gist.github.com/bradfol/541c010a6540404eca0f4a5da009c761

            import Foundation
            
            class Debouncer {
            
                // Callback to be debounced
                // Perform the work you would like to be debounced in this callback.
                var callback: (() -> Void)?
            
                private let interval: TimeInterval // Time interval of the debounce window
            
                init(interval: TimeInterval) {
                    self.interval = interval
                }
            
                private var timer: Timer?
            
                // Indicate that the callback should be called. Begins the debounce window.
                func call() {
                    // Invalidate existing timer if there is one
                    timer?.invalidate()
                    // Begin a new timer from now
                    timer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(handleTimer), userInfo: nil, repeats: false)
                }
            
                @objc private func handleTimer(_ timer: Timer) {
                    if callback == nil {
                        NSLog("Debouncer timer fired, but callback was nil")
                    } else {
                        NSLog("Debouncer timer fired")
                    }
                    callback?()
                    callback = nil
                }
            
            }
            

            【讨论】:

              【解决方案13】:

              owenoak 的解决方案对我有用。我对其进行了一些更改以适合我的项目:

              我创建了一个swift文件Dispatcher.swift

              import Cocoa
              
              // Encapsulate an action so that we can use it with NSTimer.
              class Handler {
              
                  let action: ()->()
              
                  init(_ action: ()->()) {
                      self.action = action
                  }
              
                  @objc func handle() {
                      action()
                  }
              
              }
              
              // Creates and returns a new debounced version of the passed function 
              // which will postpone its execution until after delay seconds have elapsed 
              // since the last time it was invoked.
              func debounce(delay: NSTimeInterval, action: ()->()) -> ()->() {
                  let handler = Handler(action)
                  var timer: NSTimer?
                  return {
                      if let timer = timer {
                          timer.invalidate() // if calling again, invalidate the last timer
                      }
                      timer = NSTimer(timeInterval: delay, target: handler, selector: "handle", userInfo: nil, repeats: false)
                      NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSDefaultRunLoopMode)
                      NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSEventTrackingRunLoopMode)
                  }
              }
              

              然后我在我的 UI 类中添加了以下内容:

              class func changed() {
                      print("changed")
                  }
              let debouncedChanged = debounce(0.5, action: MainWindowController.changed)
              

              与 owenoak 的答案的主要区别在于这一行:

              NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSEventTrackingRunLoopMode)
              

              没有这条线,如果 UI 失去焦点,计时器永远不会触发。

              【讨论】:

                【解决方案14】:

                场景:用户连续点击按钮,但只接受最后一个按钮,并且取消所有先前的请求。为简单起见,fetchMethod() 打印计数器值。

                1:延迟后使用执行选择器:

                Swift 5 工作示例

                import UIKit
                class ViewController: UIViewController {
                
                    var stepper = 1
                
                    override func viewDidLoad() {
                        super.viewDidLoad()
                
                
                    }
                
                
                    @IBAction func StepperBtnTapped() {
                        stepper = stepper + 1
                        NSObject.cancelPreviousPerformRequests(withTarget: self)
                        perform(#selector(updateRecord), with: self, afterDelay: 0.5)
                    }
                
                    @objc func updateRecord() {
                        print("final Count \(stepper)")
                    }
                
                }
                

                2:使用 DispatchWorkItem:

                class ViewController: UIViewController {
                      private var pendingRequestWorkItem: DispatchWorkItem?
                override func viewDidLoad() {
                      super.viewDidLoad()
                     }
                @IBAction func tapButton(sender: UIButton) {
                      counter += 1
                      pendingRequestWorkItem?.cancel()
                      let requestWorkItem = DispatchWorkItem { [weak self] in                        self?.fetchMethod()
                          }
                       pendingRequestWorkItem = requestWorkItem
                       DispatchQueue.main.asyncAfter(deadline: .now()   +.milliseconds(250),execute: requestWorkItem)
                     }
                func fetchMethod() {
                        print("fetchMethod:\(counter)")
                    }
                }
                //Output:
                fetchMethod:1  //clicked once
                fetchMethod:4  //clicked 4 times ,
                               //but previous triggers are cancelled by
                               // pendingRequestWorkItem?.cancel()
                

                refrence link

                【讨论】:

                  【解决方案15】:
                  • 免责声明:我是一名作家。

                  Throttler 可能是完成它的正确工具。

                  您可以在不使用 Throttler 之类的反应式编程的情况下进行去抖动和节流,

                  import Throttler
                  
                  // advanced debounce, running a first task immediately before initiating debounce.
                  
                  for i in 1...1000 {
                      Throttler.debounce {
                          print("debounce! > \(i)")
                      }
                  }
                  
                  // debounce! > 1
                  // debounce! > 1000
                  
                  
                  // equivalent to debounce of Combine, RxSwift.
                  
                  for i in 1...1000 {
                      Throttler.debounce(shouldRunImmediately: false) {
                          print("debounce! > \(i)")
                      }
                  }
                  
                  // debounce! > 1000
                  
                  

                  Throttler 还可以进行高级去抖动,在启动去抖动之前立即运行第一个事件,Combine 和 RxSwift 默认没有。

                  您可以,但您可能需要自己进行复杂的实现。

                  【讨论】:

                    猜你喜欢
                    • 2020-04-10
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2017-04-10
                    • 2021-11-06
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多