Skip to content

Instantly share code, notes, and snippets.

@simme
Created December 20, 2016 10:04
Show Gist options
  • Star 72 You must be signed in to star a gist
  • Fork 16 You must be signed in to fork a gist
  • Save simme/b78d10f0b29325743a18c905c5512788 to your computer and use it in GitHub Desktop.
Save simme/b78d10f0b29325743a18c905c5512788 to your computer and use it in GitHub Desktop.
Swift 3 debounce & throttle
//
// 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!)
}
}
@simme
Copy link
Author

simme commented Dec 20, 2016

Usage example:

func bar(bar: String) {
    print("Bar", bar)
}

func testThrottle() {
    let queue = DispatchQueue.global(qos: .background)
    let fn = throttle1(delay: 0.4, queue: queue, action: bar)
    for i in 0...9 {
        fn("\(i)")
        Thread.sleep(forTimeInterval: 0.2)
    }
}

testThrottle()

@danejordan
Copy link

danejordan commented May 17, 2018

Thank you!!

@danejordan
Copy link

danejordan commented Jun 3, 2018

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

@michzio
Copy link

michzio commented Apr 8, 2019

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

@erdemildiz
Copy link

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