web-dev-qa-db-fra.com

Comment puis-je annuler un appel de méthode?

J'essaie d'utiliser un UISearchView pour interroger Google Adresses. Ce faisant, lors des changements de texte pour mes UISearchBar, je fais une demande à google places. Le problème est que je préfère ne pas utiliser cet appel pour ne demander qu'une fois par 250 ms afin d'éviter un trafic réseau inutile. Je préfère ne pas écrire cette fonctionnalité moi-même, mais je le ferai si nécessaire.

J'ai trouvé: https://Gist.github.com/ShamylZakariya/54ee03228d955f458389 , mais je ne sais pas trop comment l'utiliser:

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

Voici une chose que j'ai essayé d'utiliser le code ci-dessus:

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

L'erreur résultante est Cannot invoke function with an argument list of type '(NSTimeInterval, $T5, () -> ())

Comment utiliser cette méthode, ou existe-t-il une meilleure façon de le faire dans iOS/Swift.

24
Parris

Mettez cela au niveau supérieur de votre fichier afin de ne pas vous confondre avec les règles de nom de paramètre drôles de Swift. Notez que j'ai supprimé le # pour qu'aucun des paramètres ne porte de nom:

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

Maintenant, dans votre classe actuelle, votre code ressemblera à ceci:

let searchDebounceInterval: NSTimeInterval = NSTimeInterval(0.25)
let q = dispatch_get_main_queue()
func findPlaces() {
    // ...
}
let debouncedFindPlaces = debounce(
        searchDebounceInterval,
        q,
        findPlaces
    )

Maintenant, debouncedFindPlaces est une fonction que vous pouvez appeler, et votre findPlaces ne sera pas exécuté à moins que delay ne soit passé depuis la dernière fois que vous l'avez appelée.

15
matt

Version Swift 3

1. Fonction anti-rebond de base

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. Fonction anti-rebond paramétrée

Parfois, il est utile que la fonction anti-rebond prenne un paramètre.

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

Dans l'exemple suivant, vous pouvez voir comment fonctionne la fonction anti-rebond, en utilisant un paramètre de chaîne pour identifier les appels.

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

Remarque: la fonction usleep() est uniquement utilisée à des fins de démonstration et peut ne pas être la solution la plus élégante pour une application réelle.

Résultat

Vous obtenez toujours un rappel, lorsqu'il y a un intervalle d'au moins 200 ms depuis le dernier appel.

appelé: 4
appelé: 7
appelé: 12

17
d4Rk

Si vous aimez garder les choses propres, voici une solution basée sur GCD qui peut faire ce dont vous avez besoin en utilisant une syntaxe basée sur GCD familière: https://Gist.github.com/staminajim/b5e89c6611eef81910502db2a01f1a8

DispatchQueue.main.asyncDeduped(target: self, after: 0.25) { [weak self] in
     self?.findPlaces()
}

findPlaces () ne sera appelé qu'une seule fois , 0,25 seconde après le dernier appel à asyncDuped.

11
staminajim

Créez d'abord une classe générique Debouncer:

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

Créez ensuite une sous-classe de UISearchBar qui utilise le mécanisme anti-rebond:

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

Notez que cette classe est définie comme UISearchBarDelegate. Les actions seront transmises à cette classe en tant que fermetures.

Enfin, vous pouvez l'utiliser comme ceci:

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

Notez que dans ce cas, DebounceSearchBar est déjà le délégué searchBar. Vous devez PAS définir cette sous-classe UIViewController comme délégué searchBar! N'utilisez pas non plus les fonctions de délégué. Utilisez plutôt les fermetures fournies!

5
Frédéric Adda

Ce qui suit fonctionne pour moi:

Ajoutez ce qui suit à un fichier de votre projet (je gère un fichier 'SwiftExtensions.Swift' pour des choses comme ça):

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

Ensuite, installez-le dans vos cours:

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

Vous pouvez maintenant appeler someClass.doSomething() à plusieurs reprises et il ne sauvera qu'une fois par seconde.

4
owenoak

Voici une option pour ceux qui ne veulent pas créer de classes/extensions:

Quelque part dans votre code:

var debounce_timer:Timer?

Et dans les endroits où vous voulez faire le debounce:

debounce_timer?.invalidate()
debounce_timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in 
    print ("Debounce this...") 
}
4
Khrob

La solution générale telle que fournie par la question et développée dans plusieurs des réponses, a une erreur logique qui provoque des problèmes avec des seuils de rebond courts.

En commençant par l'implémentation fournie:

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

En testant avec un intervalle de 30 millisecondes, nous pouvons créer un exemple relativement trivial qui démontre la faiblesse.

let oldDebouncerDebouncedFunction = debounce(interval: 30, queue: .main, action: exampleFunction)

DispatchQueue.global(qos: .background).async {

    oldDebouncerDebouncedFunction("1")
    oldDebouncerDebouncedFunction("2")
    sleep(.seconds(2))
    oldDebouncerDebouncedFunction("3")
}

Cela imprime

appelé: 1
appelé: 2
appelé: 3

Ceci est clairement incorrect, car le premier appel doit être rejeté. L'utilisation d'un seuil anti-rebond plus long (comme 300 millisecondes) résoudra le problème. La racine du problème est une fausse attente selon laquelle la valeur de DispatchTime.now() sera égale à deadline passée à asyncAfter(deadline: DispatchTime). L'intention de la comparaison now.rawValue >= when.rawValue Est de comparer réellement l'échéance attendue à l'échéance "la plus récente". Avec de petits seuils anti-rebond, la latence de asyncAfter devient un problème très important à considérer.

Il est cependant facile à corriger et le code peut être rendu plus concis par-dessus. En choisissant soigneusement le moment d'appeler .now(), et en assurant la comparaison de l'échéance réelle avec l'échéance la plus récente, je suis arrivé à cette solution. Ce qui est correct pour toutes les valeurs de threshold. Portez une attention particulière aux points # 1 et # 2, car ils sont identiques sur le plan syntaxique, mais ils seront différents si plusieurs appels sont effectués avant l'envoi du travail.

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

Utilitaires

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

Espérons que cette réponse aidera quelqu'un d'autre qui a rencontré un comportement inattendu avec la solution de curry de fonction.

4
allenh

Malgré plusieurs bonnes réponses ici, j'ai pensé partager mon approche préférée (pure Swift) pour éliminer les recherches entrées par l'utilisateur ...

1) Ajoutez cette classe simple ( 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) Incluez éventuellement ce test unitaire ( 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) Utilisez-le où vous voulez retarder le traitement (par exemple UISearchBarDelegate ):

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    Debounce<String>.input(searchText, comparedAgainst: searchBar.text ?? "") {
        self.filterResults($0)
    }
}

Le principe de base est que nous retardons simplement le traitement du texte saisi de 0,5 seconde. À ce moment-là, nous comparons la chaîne que nous avons obtenue de l'événement avec la valeur actuelle de la barre de recherche. S'ils correspondent, nous supposons que l'utilisateur a interrompu la saisie du texte et nous procédons au filtrage.

Comme il est générique, il fonctionne avec tout type de valeur équivalente.

Étant donné que le module Dispatch a été inclus dans la bibliothèque principale Swift depuis la version 3, cette classe peut également être utilisée avec des plates-formes non Apple.

3
quickthyme

J'ai utilisé cette bonne vieille méthode inspirée d'Objective-C:

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

Notez que la méthode appelée updateSearch doit être marquée @objc!

@objc private func updateSearch(with text: String) {
    // Do stuff here   
}

Le gros avantage de cette méthode est que je peux passer des paramètres (ici: la chaîne de recherche). Avec la plupart des Debouncers présentés ici, ce n'est pas le cas ...

2
Frédéric Adda

Une autre implémentation anti-rebond utilisant la classe, vous pouvez trouver utile: https://github.com/webadnan/Swift-debouncer

2
SM Adnan

Voici une implémentation anti-rebond pour 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
    }

}
1
Brad

Scénario: L'utilisateur appuie sur le bouton en continu, mais seule la dernière est acceptée et toute demande précédente est annulée. Pour rester simple, fetchMethod () imprime la valeur du compteur.

1: Utilisation du sélecteur Perform Après un délai:

exemple de travail 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: Utilisation de 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()

lien de réfraction

0
Muhammad Naveed

Quelques améliorations subtiles sur quickthyme 's excellente réponse :

  1. Ajoutez un paramètre delay, peut-être avec une valeur par défaut.
  2. Faites de Debounce un enum au lieu d'un class, afin que vous puissiez éviter d'avoir à déclarer un 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)
        }
    }
}

Il n'est également pas nécessaire de déclarer explicitement le type générique sur le site d'appel - il peut être déduit. Par exemple, si vous souhaitez utiliser Debounce avec un UISearchController, dans updateSearchResults(for:) (méthode requise de UISearchResultsUpdating), vous feriez ceci:

func updateSearchResults(for searchController: UISearchController) {
    guard let text = searchController.searchBar.text else { return }

    Debounce.input(text, current: searchController.searchBar.text ?? "") {
        // ...
    }

}
0
Scott Gardner

la solution d'owenoak fonctionne pour moi. Je l'ai un peu modifié pour l'adapter à mon projet:

J'ai créé un fichier Swift 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)
    }
}

J'ai ensuite ajouté ce qui suit dans ma classe d'interface utilisateur:

class func changed() {
        print("changed")
    }
let debouncedChanged = debounce(0.5, action: MainWindowController.changed)

La principale différence avec la réponse d'owenoak est cette ligne:

NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSEventTrackingRunLoopMode)

Sans cette ligne, le temporisateur ne se déclenche jamais si l'interface utilisateur perd le focus.

0
Tyler Long