'How can I debounce a method call?
I'm trying to use a UISearchView
to query google places. In doing so, on text change calls for my UISearchBar
, I'm making a request to google places. The problem is I'd rather debounce this call to only request once per 250 ms in order to avoid unnecessary network traffic. I'd rather not write this functionality myself, but I will if I need to.
I found: https://gist.github.com/ShamylZakariya/54ee03228d955f458389 , but I'm not quite sure how to use it:
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()
}
}
}
}
Here is one thing I've tried using the above code:
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
)
}
The resulting error is Cannot invoke function with an argument list of type '(NSTimeInterval, $T5, () -> ())
How do I use this method, or is there a better way to do this in iOS/Swift.
Solution 1:[1]
Put this at the top level of your file so as not to confuse yourself with Swift's funny parameter name rules. Notice that I've deleted the #
so that now none of the parameters have names:
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()
}
}
}
}
Now, in your actual class, your code will look like this:
let searchDebounceInterval: NSTimeInterval = NSTimeInterval(0.25)
let q = dispatch_get_main_queue()
func findPlaces() {
// ...
}
let debouncedFindPlaces = debounce(
searchDebounceInterval,
q,
findPlaces
)
Now debouncedFindPlaces
is a function which you can call, and your findPlaces
won't be executed unless delay
has passed since the last time you called it.
Solution 2:[2]
Here's an option for those not wanting to create classes/extensions:
Somewhere in your code:
var debounce_timer:Timer?
And in places you want to do the debounce:
debounce_timer?.invalidate()
debounce_timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
print ("Debounce this...")
}
Solution 3:[3]
If you like to keep things clean, here's a GCD based solution that can do what you need using familiar GCD based syntax: https://gist.github.com/staminajim/b5e89c6611eef81910502db2a01f1a83
DispatchQueue.main.asyncDeduped(target: self, after: 0.25) { [weak self] in
self?.findPlaces()
}
findPlaces() will only get called one time, 0.25 seconds after the last call to asyncDuped.
Solution 4:[4]
Swift 3 version
1. Basic debounce function
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. Parameterized debounce function
Sometimes it's useful to be have the debounce function take a parameter.
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. Example
In the following example you can see, how the debouncing works, using a string parameter to identify the calls.
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")
}
Note: The usleep()
function is only used for demo purposes and may not be the most elegant solution for a real app.
Result
You always get a callback, when there is an interval of at least 200ms since the last call.
called: 4
called: 7
called: 12
Solution 5:[5]
Despite several great answers here, I thought I'd share my favorite (pure Swift) approach for debouncing user entered searches...
1) Add this simple class (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) Optionally include this unit test (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) Use it wherever you want to delay processing (e.g. UISearchBarDelegate):
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
Debounce<String>.input(searchText, comparedAgainst: searchBar.text ?? "") {
self.filterResults($0)
}
}
Basic premise is that we are just delaying the processing of the input text by 0.5 seconds. At that time, we compare the string we got from the event with the current value of the search bar. If they match, we assume that the user has paused entering text, and we proceed with the filtering operation.
As it is generic, it works with any type of equatable value.
Since the Dispatch module has been included in the Swift core library since version 3, this class is safe to use with non-Apple platforms as well.
Solution 6:[6]
First, create a Debouncer generic class:
//
// 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)
}
}
Then create a subclass of UISearchBar that uses the debounce mechanism:
//
// 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 ?? "")
}
}
}
}
Note that this class is set as the UISearchBarDelegate. Actions will be passed to this class as closures.
Finally, you can use it like so:
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
}
}
Note that in that case, the DebounceSearchBar is already the searchBar delegate. You should NOT set this UIViewController subclass as the searchBar delegate! Nor use delegate functions. Use the provided closures instead!
Solution 7:[7]
The following is working for me:
Add the below to some file within your project (I maintain a 'SwiftExtensions.swift' file for things like this):
// 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)
}
}
Then set it up in your classes:
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()
}
}
You can now call someClass.doSomething()
repeatedly and it will only save once per second.
Solution 8:[8]
I used this good old Objective-C inspired method:
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)
}
Note that the called method updateSearch
must be marked @objc !
@objc private func updateSearch(with text: String) {
// Do stuff here
}
The big advantage of this method is that I can pass parameters (here: the search string). With most of Debouncers presented here, that is not the case ...
Solution 9:[9]
The general solution as provided by the question and built upon in several of the answers, has a logic mistake that causes problems with short debounce thresholds.
Starting with the provided implementation:
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)
}
}
}
}
Testing with an interval of 30 milliseconds, we can create a relatively trivial example that demonstrates the weakness.
let oldDebouncerDebouncedFunction = debounce(interval: 30, queue: .main, action: exampleFunction)
DispatchQueue.global(qos: .background).async {
oldDebouncerDebouncedFunction("1")
oldDebouncerDebouncedFunction("2")
sleep(.seconds(2))
oldDebouncerDebouncedFunction("3")
}
This prints
called: 1
called: 2
called: 3
This is clearly incorrect, because the first call should be debounced. Using a longer debounce threshold (such as 300 milliseconds) will fix the problem. The root of the problem is a false expectation that the value of DispatchTime.now()
will be equal to the deadline
passed to asyncAfter(deadline: DispatchTime)
. The intention of the comparison now.rawValue >= when.rawValue
is to actually compare the expected deadline to the "most recent" deadline. With small debounce thresholds, the latency of asyncAfter
becomes a very important problem to think about.
It's easy to fix though, and the code can be made more concise on top of it. By carefully choosing when to call .now()
, and ensuring the comparison of the actual deadline with most recently scheduled deadline, I arrived at this solution. Which is correct for all values of threshold
. Pay special attention to #1 and #2 as they are the same syntactically, but will be different if multiple calls are made before the work is dispatched.
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)
}
}
}
}
Utilities
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
}
}
Hopefully, this answer will help someone else that has encountered unexpected behavior with the function currying solution.
Solution 10:[10]
Here you have totally Swift 5 friendly and smooth solution ??
You can use it for example when detecting tableView scrolls to bottom.
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!")
}
Solution 11:[11]
A couple subtle improvements on quickthyme's excellent answer:
- Add a
delay
parameter, perhaps with a default value. - Make
Debounce
anenum
instead of aclass
, so you can skip having to declare aprivate 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)
}
}
}
It's also not necessary to explicitly declare the generic type at the call site — it can be inferred. For example, if you want to use Debounce
with a UISearchController
, in updateSearchResults(for:)
(required method of UISearchResultsUpdating
), you would do this:
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else { return }
Debounce.input(text, current: searchController.searchBar.text ?? "") {
// ...
}
}
Solution 12:[12]
Here is a debounce implementation for 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
}
}
Solution 13:[13]
owenoak's solution works for me. I changed it a little bit to fit my project:
I created a swift file 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)
}
}
Then I added the following in my UI class:
class func changed() {
print("changed")
}
let debouncedChanged = debounce(0.5, action: MainWindowController.changed)
The key difference from owenoak's anwer is this line:
NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSEventTrackingRunLoopMode)
Without this line, the timer never triggers if the UI loses focus.
Solution 14:[14]
Scenario: User taps on button continuously but only last one is accepted and all previous request is cancelled.To keep it simple fetchMethod() prints the counter value.
1: Using Perform Selector After a delay:
working example 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:Using 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()
Solution 15:[15]
- disclaimer: I am a writer.
Throttler could be the right tool to get it done.
You can do debounce and throttle without going reactive programming using Throttler like,
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 also can do advanced debounce, running a first event immediately before initiating debounce that Combine and RxSwift don't have by default.
You could, but you may need a complex implementation yourself for that.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow