web-dev-qa-db-ja.com

メソッド呼び出しをデバウンスするにはどうすればよいですか?

UISearchViewを使用してGoogleの場所をクエリしようとしています。その際、UISearchBarのテキスト変更呼び出しで、Googleプレイスにリクエストを送信します。問題は、不要なネットワークトラフィックを回避するために、この呼び出しを250ミリ秒ごとに1回だけ要求するようにデバウンスすることです。私はこの機能を自分で書くのではなく、必要に応じて記述します。

私は見つけました: https://Gist.github.com/ShamylZakariya/54ee03228d955f458389 ですが、それをどのように使用するかよくわかりません:

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

上記のコードを使用して試したのは次のとおりです。

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

結果のエラーはCannot invoke function with an argument list of type '(NSTimeInterval, $T5, () -> ())です

この方法を使用するにはどうすればよいですか、またはiOS/Swiftでこれを行うより良い方法があります。

24
Parris

これをファイルのトップレベルに配置して、Swiftの面白いパラメーター名のルールと混同しないようにしてください。 #を削除しているため、どのパラメーターにも名前がありません。

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

これで、実際のクラスでは、コードは次のようになります。

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

これでdebouncedFindPlacesは呼び出し可能な関数になり、findPlacesは最後に呼び出してからdelayが経過しない限り実行されません。

15
matt

Swift 3バージョン

1.基本的なデバウンス機能

_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.パラメータ化されたデバウンス機能

場合によっては、デバウンス関数にパラメーターを指定すると便利です。

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

次の例では、文字列パラメーターを使用して呼び出しを識別することにより、デバウンスがどのように機能するかを確認できます。

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

注:usleep()関数はデモ目的でのみ使用され、実際のアプリでは最もエレガントなソリューションではない場合があります。

結果

最後の呼び出しから少なくとも200ミリ秒の間隔がある場合、常にコールバックを取得します。

呼ばれる:4
呼ばれる:7
呼ばれる:12

17
d4Rk

クリーンな状態を維持したい場合は、おなじみのGCDベースの構文を使用して必要なことを実行できるGCDベースのソリューションを次に示します。 https://Gist.github.com/staminajim/b5e89c6611eef81910502db2a01f1a8

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

findPlaces()は、asyncDupedへの最後の呼び出しから0.25秒後に1回だけ呼び出されます。

11
staminajim

まず、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)
    }
}

次に、デバウンスメカニズムを使用するUISearchBarのサブクラスを作成します。

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

このクラスはUISearchBarDelegateとして設定されていることに注意してください。アクションはクロージャーとしてこのクラスに渡されます。

最後に、次のように使用できます。

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

その場合、DebounceSearchBarは既にsearchBarデリゲートであることに注意してください。 [〜#〜] not [〜#〜]このUIViewControllerサブクラスをsearchBarデリゲートとして設定してください!デリゲート関数も使用しません。代わりに提供されたクロージャーを使用してください!

5
Frédéric Adda

以下は私のために働いています:

以下をプロジェクト内のいくつかのファイルに追加します(私はこのようなもののために 'SwiftExtensions.Swift'ファイルを維持しています):

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

次に、クラスに設定します。

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

someClass.doSomething()を繰り返し呼び出すことができるようになり、毎秒1回だけ保存されます。

4
owenoak

クラス/拡張を作成したくない人のためのオプションがあります:

コードのどこかに:

var debounce_timer:Timer?

そして、あなたがデバウンスをしたい場所で:

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

質問によって提供され、いくつかの回答に基づいて作成された一般的な解決策には、短いデバウンスしきい値で問題を引き起こす論理ミスがあります。

提供された実装から始めます。

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

30ミリ秒の間隔でテストすると、弱点を示す比較的簡単な例を作成できます。

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

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

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

これはプリント

呼ばれる:1
呼ばれる:2
呼ばれる:3

最初のコールはデバウンスされる必要があるため、これは明らかに正しくありません。より長いデバウンスしきい値(300ミリ秒など)を使用すると、問題が解決します。問題の根本は、DispatchTime.now()の値がasyncAfter(deadline: DispatchTime)に渡されるdeadlineと等しいという誤った期待です。比較_now.rawValue >= when.rawValue_の目的は、実際に期待される期限と「最新の」期限を比較することです。デバウンスしきい値が小さい場合、asyncAfterのレイテンシは考慮する必要がある非常に重要な問題になります。

ただし、修正は簡単で、コードをさらに簡潔にすることができます。 .now()を呼び出すタイミングを慎重に選択し、実際の期限と最近スケジュールされた期限を確実に比較することで、このソリューションにたどり着きました。これは、thresholdのすべての値に適しています。 #1と#2は構文的には同じなので特に注意してください。ただし、作業がディスパッチされる前に複数の呼び出しが行われる場合は異なります。

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

ユーティリティ

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

うまくいけば、この答えは関数カリー化ソリューションで予期しない動作に遭遇した他の誰かを助けるでしょう。

4
allenh

ここにいくつかの素晴らしい答えがあるにもかかわらず、ユーザーが入力した検索をデバウンスするための私のお気に入りの(pure Swift)アプローチを共有したいと思いました...

1)この単純なクラスを追加します(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)オプションでこの単体テストを含める(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)処理を遅らせたい場所で使用します(例:UISearchBarDelegate):

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

基本的な前提は、入力テキストの処理を0.5秒だけ遅らせることです。そのとき、イベントから取得した文字列を検索バーの現在の値と比較します。それらが一致する場合、ユーザーがテキストの入力を一時停止したと見なし、フィルタリング操作を続行します。

それは一般的であるため、任意のタイプの等値で機能します。

DispatchモジュールがSwiftコアライブラリバージョン3以降に含まれているため、このクラスは、Apple以外のプラットフォームでも安全に使用できます

3
quickthyme

私はこの古き良き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)
}

呼び出されたメソッドupdateSearchは@objcとマークする必要があることに注意してください。

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

この方法の大きな利点はパラメータを渡すことができる(ここでは検索文字列)です。ここに示されているほとんどのデバウンサーでは、そうではありません...

2
Frédéric Adda

クラスを使用した別のデバウンス実装、あなたは役に立つかもしれません: https://github.com/webadnan/Swift-debouncer

2
SM Adnan

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

シナリオ:ユーザーはボタンを継続的にタップしますが、最後の1つだけが受け入れられ、前の要求はすべてキャンセルされます。単純に保つために、fetchMethod()はカウンター値を出力します。

1:遅延後の実行セレクターの使用:

動作例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: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()

参照リンク

0
Muhammad Naveed

quickthyme のいくつかの微妙な改善 優れた答え

  1. delayパラメータを追加します。おそらくデフォルト値を使用します。
  2. Debounceの代わりにenumclassにすると、_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)
        }
    }
}
_

また、呼び出しサイトでジェネリック型を明示的に宣言する必要もありません—推測できます。たとえば、DebounceUISearchControllerupdateSearchResults(for:)で使用する場合(UISearchResultsUpdatingの必須メソッド)、次のようにします。

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

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

}
_
0
Scott Gardner

owenoakの解決策は私にとってうまくいきます。私のプロジェクトに合わせて少し変更しました:

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

次に、UIクラスに次のコードを追加しました。

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

Owenoakの回答との主な違いは次の行です。

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

この行がないと、UIがフォーカスを失ってもタイマーはトリガーされません。

0
Tyler Long