Created
December 20, 2016 10:04
-
-
Save simme/b78d10f0b29325743a18c905c5512788 to your computer and use it in GitHub Desktop.
Swift 3 debounce & throttle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// debounce-throttle.swift | |
// | |
// Created by Simon Ljungberg on 19/12/16. | |
// License: MIT | |
// | |
import Foundation | |
extension TimeInterval { | |
/** | |
Checks if `since` has passed since `self`. | |
- Parameter since: The duration of time that needs to have passed for this function to return `true`. | |
- Returns: `true` if `since` has passed since now. | |
*/ | |
func hasPassed(since: TimeInterval) -> Bool { | |
return Date().timeIntervalSinceReferenceDate - self > since | |
} | |
} | |
/** | |
Wraps a function in a new function that will only execute the wrapped function if `delay` has passed without this function being called. | |
- Parameter delay: A `DispatchTimeInterval` to wait before executing the wrapped function after last invocation. | |
- Parameter queue: The queue to perform the action on. Defaults to the main queue. | |
- Parameter action: A function to debounce. Can't accept any arguments. | |
- Returns: A new function that will only call `action` if `delay` time passes between invocations. | |
*/ | |
func debounce(delay: DispatchTimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void { | |
var currentWorkItem: DispatchWorkItem? | |
return { | |
currentWorkItem?.cancel() | |
currentWorkItem = DispatchWorkItem { action() } | |
queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) | |
} | |
} | |
/** | |
Wraps a function in a new function that will only execute the wrapped function if `delay` has passed without this function being called. | |
Accepsts an `action` with one argument. | |
- Parameter delay: A `DispatchTimeInterval` to wait before executing the wrapped function after last invocation. | |
- Parameter queue: The queue to perform the action on. Defaults to the main queue. | |
- Parameter action: A function to debounce. Can accept one argument. | |
- Returns: A new function that will only call `action` if `delay` time passes between invocations. | |
*/ | |
func debounce1<T>(delay: DispatchTimeInterval, queue: DispatchQueue = .main, action: @escaping ((T) -> Void)) -> (T) -> Void { | |
var currentWorkItem: DispatchWorkItem? | |
return { (p1: T) in | |
currentWorkItem?.cancel() | |
currentWorkItem = DispatchWorkItem { action(p1) } | |
queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) | |
} | |
} | |
/** | |
Wraps a function in a new function that will only execute the wrapped function if `delay` has passed without this function being called. | |
Accepsts an `action` with two arguments. | |
- Parameter delay: A `DispatchTimeInterval` to wait before executing the wrapped function after last invocation. | |
- Parameter queue: The queue to perform the action on. Defaults to the main queue. | |
- Parameter action: A function to debounce. Can accept two arguments. | |
- Returns: A new function that will only call `action` if `delay` time passes between invocations. | |
*/ | |
func debounce2<T, U>(delay: DispatchTimeInterval, queue: DispatchQueue = .main, action: @escaping ((T, U) -> Void)) -> (T, U) -> Void { | |
var currentWorkItem: DispatchWorkItem? | |
return { (p1: T, p2: U) in | |
currentWorkItem?.cancel() | |
currentWorkItem = DispatchWorkItem { action(p1, p2) } | |
queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) | |
} | |
} | |
/** | |
Wraps a function in a new function that will throttle the execution to once in every `delay` seconds. | |
- Parameter delay: A `TimeInterval` specifying the number of seconds that needst to pass between each execution of `action`. | |
- Parameter queue: The queue to perform the action on. Defaults to the main queue. | |
- Parameter action: A function to throttle. | |
- Returns: A new function that will only call `action` once every `delay` seconds, regardless of how often it is called. | |
*/ | |
func throttle(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void { | |
var currentWorkItem: DispatchWorkItem? | |
var lastFire: TimeInterval = 0 | |
return { | |
guard currentWorkItem == nil else { return } | |
currentWorkItem = DispatchWorkItem { | |
action() | |
lastFire = Date().timeIntervalSinceReferenceDate | |
currentWorkItem = nil | |
} | |
delay.hasPassed(since: lastFire) ? queue.async(execute: currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) | |
} | |
} | |
/** | |
Wraps a function in a new function that will throttle the execution to once in every `delay` seconds. | |
Accepts an `action` with one argument. | |
- Parameter delay: A `TimeInterval` specifying the number of seconds that needst to pass between each execution of `action`. | |
- Parameter queue: The queue to perform the action on. Defaults to the main queue. | |
- Parameter action: A function to throttle. Can accept one argument. | |
- Returns: A new function that will only call `action` once every `delay` seconds, regardless of how often it is called. | |
*/ | |
func throttle1<T>(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping ((T) -> Void)) -> (T) -> Void { | |
var currentWorkItem: DispatchWorkItem? | |
var lastFire: TimeInterval = 0 | |
return { (p1: T) in | |
guard currentWorkItem == nil else { return } | |
currentWorkItem = DispatchWorkItem { | |
action(p1) | |
lastFire = Date().timeIntervalSinceReferenceDate | |
currentWorkItem = nil | |
} | |
delay.hasPassed(since: lastFire) ? queue.async(execute: currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) | |
} | |
} | |
/** | |
Wraps a function in a new function that will throttle the execution to once in every `delay` seconds. | |
Accepts an `action` with two arguments. | |
- Parameter delay: A `TimeInterval` specifying the number of seconds that needst to pass between each execution of `action`. | |
- Parameter queue: The queue to perform the action on. Defaults to the main queue. | |
- Parameter action: A function to throttle. Can accept two arguments. | |
- Returns: A new function that will only call `action` once every `delay` seconds, regardless of how often it is called. | |
*/ | |
func throttle2<T, U>(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping ((T, U) -> Void)) -> (T, U) -> Void { | |
var currentWorkItem: DispatchWorkItem? | |
var lastFire: TimeInterval = 0 | |
return { (p1: T, p2: U) in | |
guard currentWorkItem == nil else { return } | |
currentWorkItem = DispatchWorkItem { | |
action(p1, p2) | |
lastFire = Date().timeIntervalSinceReferenceDate | |
currentWorkItem = nil | |
} | |
delay.hasPassed(since: lastFire) ? queue.async(execute: currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) | |
} | |
} |
Thank you!!
If you want throttle to trigger instantaneously on the first call instead of waiting delay
seconds:
func throttle(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void {
var lastFireTime = 0.0
var currentWorkItem:DispatchWorkItem?
return {
if delay.hasPassed(since: lastFireTime) {
currentWorkItem = DispatchWorkItem {
action()
lastFireTime = Date.timeIntervalSinceReferenceDate
}
queue.async(execute: currentWorkItem!)
}
}
}
Above code has huge flaw. You cannot use it as independent function calls. You can only wrap one action closure and call this wrapped several times. If you have some delegation, or async callback, closure called periodically, notification. Then you cannot debounce or throttle with this code.
Important! You should keep references to lastFireTime or DispatchWarkItem object to be able to use this between multiple independent action calls.
I recommend to use something like this:
class Throttler {
var currentWorkItem: DispatchWorkItem?
var lastFire: TimeInterval = 0
func throttle(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void {
return { [weak self] in
guard let self = self else { return }
guard self.currentWorkItem == nil else { return }
self.currentWorkItem = DispatchWorkItem {
action()
self.lastFire = Date().timeIntervalSinceReferenceDate
self.currentWorkItem = nil
}
delay.hasPassed(since: self.lastFire) ? queue.async(execute: self.currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: self.currentWorkItem!)
}
}
}
class Debouncer {
var currentWorkItem: DispatchWorkItem?
func debounce(delay: DispatchTimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void {
return { [weak self] in
guard let self = self else { return }
self.currentWorkItem?.cancel()
self.currentWorkItem = DispatchWorkItem { action() }
queue.asyncAfter(deadline: .now() + delay, execute: self.currentWorkItem!)
}
}
}
And use it like this:
let debounceReload = debouncer.debounce(delay: .seconds(2)) {
print("Debounced reload. \(Date.timeIntervalSinceReferenceDate)")
self.reloadData()
}
print("Before debounced reload. \(Date.timeIntervalSinceReferenceDate)")
debounceReload()
Then you can achieve:
Before debounced reload. 576412830.897752
Before debounced reload. 576412830.898164
Debounced reload. 576412832.937679
NSObject.cancelPreviousPerformRequests(withTarget: self)
perform(#selector(updateSearch), with: searchText, afterDelay: 0.3)
@objc func updateSearch() { .... }
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage example: