How can I debounce a method call?

45.9k Views Asked by At

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.

16

There are 16 best solutions below

4
On BEST ANSWER

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.

0
On

From iOS 13 With Combine

import Combine in your .swift file and Create a Debouncer class

import Combine

class Debouncer {
    // seconds to add in debounce
    private var interval: TimeInterval

    // A type-erasing cancellable object that executes a provided closure when canceled.(Stores the publisher)
    private var cancellable: AnyCancellable? = nil

    // publisher to send a event/value (Int is just an example, you can use any dataType)
    private var publisher = PassthroughSubject<Int, Never>()
    
    init(seconds: TimeInterval) {
        self.interval = seconds
    }

    // CompletionHandler which debounces a callback, 
    // to be called at most once within `delay` seconds.
   // Using debounce function of publlisher, It Publishes elements only after a specified time interval elapses between events.
    func debounce(action: @escaping (() -> Void)) {
         // add the listener to receive when publisher send's the value
        cancellable = publisher
            .debounce(for: .seconds(interval), scheduler: DispatchQueue.main)
            .sink { _ in
                action()
                self.cancellable = nil
            }

        // publish the events 
        publisher.send(0)
    }
}

Use this class in your viewController like this

class ViewController: UIViewController {

   private var debouncer = Debouncer(seconds:0.6)
   override func viewDidLoad() {
        super.viewDidLoad()

        print("Start Calling")
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
            self.reloadTable(number: 1)
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
                self.reloadTable(number: 2)
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5){
                    self.reloadTable(number: 3)
                }
            }
        }

  }

   func reloadTable(number:Int){
        debouncer.debounce {
            print("Table Reloading Number \(number)")
        }
    }
}

Here i am calling reloadTable function 3 times after every 0.5 seconds and it will print

Start Calling
Table Reloading Number 3

if we keep debounce time < 0.5 sec then it will print

private var debouncer = Debouncer(seconds:0.4)

Start Calling
Table Reloading Number 1
Table Reloading Number 2
Table Reloading Number 3

0
On

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!

0
On

Swift 5.7

Note that it is only available in iOS 16.0 or newer.

var task: Task<(), Never>?

func debounce(interval: Duration = .nanoseconds(10000),
              operation: @escaping () -> Void) {
    task?.cancel()

    task = Task {
        do {
            try await Task.sleep(for: interval)
            operation()
        } catch {
            // TODO
        }
    }
}

You can use it like

for i in 0...1000 {
    debounce {
        print(i)
    }
}

// 0
// 25
// 81
// 1000

Prior to iOS 16

import Foundation

// < iOS 16

var task: Task<(), Never>?

func debounce(seconds: Double = 1.0,
              operation: @escaping () -> Void) {
    task?.cancel()

    task = Task {
        do {
            try await Task.sleep(seconds: seconds)
            operation()
        } catch {
            // TODO
        }
    }
}

for i in 0...1000 {
    debounce(seconds: 1.0 * 0.00001) {
        print(i)
    }
}

// 0
// 123
// 1000

extension Task where Success == Never, Failure == Never {
    static func sleep(seconds: Double) async throws {
        let duration = UInt64(seconds * 1_000_000_000)
        try await Task.sleep(nanoseconds: duration)
    }
}

1
On

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!")
}
3
On

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.

3
On

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...") 
}
0
On

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 ...

0
On

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.

1
On

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.

0
On

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.

0
On

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

reference link

0
On

A couple subtle improvements on quickthyme's excellent answer:

  1. Add a delay parameter, perhaps with a default value.
  2. Make Debounce an enum instead of a class, so you can skip having to declare a 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)
        }
    }
}

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 ?? "") {
        // ...
    }

}
4
On

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

0
On

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.

0
On

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
    }

}