Skip to content

Instantly share code, notes, and snippets.

@ppave
Forked from simme/debounce-throttle.swift
Created June 4, 2021 13:28
Show Gist options
  • Save ppave/7a0b278e56ecfce0654686c11fb944e8 to your computer and use it in GitHub Desktop.
Save ppave/7a0b278e56ecfce0654686c11fb944e8 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!)
}
}
@ppave
Copy link
Author

ppave commented Jun 7, 2021

private var currentWorkItems: [String: DispatchWorkItem] = [:]
private var lastFires: [String: TimeInterval] = [:]

/// Throttles `action` execution to once in every `delay` seconds.
public func throttle(delay: TimeInterval,
                     queue: DispatchQueue = .main,
                     actionId: String = #function,
                     action: @escaping () -> Void) {
    guard currentWorkItems[actionId] == nil else { return }
    currentWorkItems[actionId] = DispatchWorkItem {
        action()
        lastFires[actionId] = Date().timeIntervalSinceReferenceDate
        currentWorkItems[actionId] = nil
    }
    let isTimePassed = Date().timeIntervalSinceReferenceDate - lastFires[actionId, default: 0] > delay
    isTimePassed ?
        queue.async(execute: currentWorkItems[actionId]!) :
        queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItems[actionId]!)
}

class ThrottleTests: XCTestCase {
    func testOneAction() throws {
        func function1(_ e: XCTestExpectation) {
            throttle(delay: 0.5) {
                e.fulfill()
            }
        }

        let e1 = expectation(description: "")
        let e2 = expectation(description: "")
        // This expectation should not be fulfilled
        e2.isInverted = true

        function1(e1)
        function1(e2)

        waitForExpectations(timeout: 1)
    }

    func testOneActionTimeExpired() throws {
        func function1(_ e: XCTestExpectation) {
            throttle(delay: 0.5) {
                e.fulfill()
            }
        }

        let e1 = expectation(description: "")
        let e2 = expectation(description: "")

        DispatchQueue.global().async {
            function1(e1)
            sleep(1)
            function1(e2)
        }

        waitForExpectations(timeout: 2)
    }

    func testSeveralActions() throws {
        func function1(_ e: XCTestExpectation) {
            throttle(delay: 0.5) {
                e.fulfill()
            }
        }

        func function2(_ e: XCTestExpectation) {
            throttle(delay: 0.5) {
                e.fulfill()
            }
        }

        let e1 = expectation(description: "")
        let e2 = expectation(description: "")

        function1(e1)
        function2(e2)

        waitForExpectations(timeout: 1)
    }
}

@ppave
Copy link
Author

ppave commented Jun 14, 2022

extension DispatchQueue {
    private static var onceTracker = [String]()

    class func once(file: String = #file,
                    function: String = #function,
                    line: Int = #line,
                    block: () -> Void) {
        let token = "\(file):\(function):\(line)"
        once(token: token, block: block)
    }

    class func once(token: String,
                    block: () -> Void) {
        objc_sync_enter(self)
        defer { objc_sync_exit(self) }

        guard !onceTracker.contains(token) else { return }

        onceTracker.append(token)
        block()
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment