如何去除方法调用?

时间:2014-11-25 00:21:46

标签: ios swift throttling

我正在尝试使用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中有更好的方法。

14 个答案:

答案 0 :(得分:15)

将其放在文件的顶级中,以免混淆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,否则{{1}}将无法执行。

答案 1 :(得分:15)

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。实施例

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

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()功能仅用于演示目的,可能不是真正应用的最佳解决方案。

结果

当自上次通话以来至少有200ms的间隔时,您总会收到回叫。

  

叫:4
  叫:7
  叫:12

答案 2 :(得分:4)

以下内容对我有用:

将以下内容添加到项目中的某个文件中(我维护一个&#39; SwiftExtensions.swift&#39;文件用于此类内容):

// 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(),每秒只能保存一次。

答案 3 :(得分:4)

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

DispatchQueue.main.asyncDeduped(target: self, after: 0.25) { [weak self] in
     self?.findPlaces()
}
在最后一次调用asyncDuped的0.25秒之后,

findPlaces()只会被调用一次

答案 4 :(得分:2)

使用类的另一个debounce实现,您可能会觉得有用: https://github.com/webadnan/swift-debouncer

答案 5 :(得分:2)

我使用了旧的受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   
}

此方法的最大优点是我可以传递参数(此处为搜索字符串)。此处介绍了大多数防弹器,事实并非如此……

答案 6 :(得分:2)

该问题提供的通用解决方案基于多个答案,该逻辑解决方案会导致逻辑错误,导致反跳阈值较短。

从提供的实现开始:

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()的值等于传递给deadline的{​​{1}}的错误期望。比较asyncAfter(deadline: DispatchTime)的目的是实际上将预期截止日期与“最新”截止日期进行比较。在防抖动阈值较小的情况下,now.rawValue >= when.rawValue的延迟成为要考虑的非常重要的问题。

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

threshold

实用程序

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)
            }
        }
    }
}

希望此答案将对使用函数循环解决方案遇到意外行为的其他人有所帮助。

答案 7 :(得分:1)

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)
        }
    }
}

也不必在调用站点上显式声明泛型—可以推断出它。例如,如果要在DebounceUISearchController的必需方法)中将updateSearchResults(for:)UISearchResultsUpdating一起使用,请执行以下操作:

func updateSearchResults(for searchController: UISearchController) {
    guard let text = searchController.searchBar.text else { return }

    Debounce.input(text, current: searchController.searchBar.text ?? "") {
        // ...
    }

}

答案 8 :(得分:1)

这里你有完全 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!")
}

答案 9 :(得分:0)

owenoak的解决方案适合我。我改变了一点以适应我的项目:

我创建了一个快速文件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's anwer的主要区别在于:

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

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

答案 10 :(得分:0)

以下是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
    }

}

答案 11 :(得分:0)

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

代码中的某处:

var debounce_timer:Timer?

在要进行反跳的地方:

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

答案 12 :(得分:0)

尽管这里有几个不错的答案,但我想我会分享我最喜欢的(纯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平台

答案 13 :(得分:0)

方案:用户连续点击按钮,但只接受最后一个,所有先前的请求被取消。为保持简单,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