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でこれを行うより良い方法があります。
これをファイルのトップレベルに配置して、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
が経過しない限り実行されません。
_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()
}
}
}
}
_
場合によっては、デバウンス関数にパラメーターを指定すると便利です。
_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)
}
}
}
}
_
次の例では、文字列パラメーターを使用して呼び出しを識別することにより、デバウンスがどのように機能するかを確認できます。
_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
クリーンな状態を維持したい場合は、おなじみの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回だけ呼び出されます。
まず、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デリゲートとして設定してください!デリゲート関数も使用しません。代わりに提供されたクロージャーを使用してください!
以下は私のために働いています:
以下をプロジェクト内のいくつかのファイルに追加します(私はこのようなもののために '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回だけ保存されます。
クラス/拡張を作成したくない人のためのオプションがあります:
コードのどこかに:
var debounce_timer:Timer?
そして、あなたがデバウンスをしたい場所で:
debounce_timer?.invalidate()
debounce_timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
print ("Debounce this...")
}
質問によって提供され、いくつかの回答に基づいて作成された一般的な解決策には、短いデバウンスしきい値で問題を引き起こす論理ミスがあります。
提供された実装から始めます。
_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
}
}
_
うまくいけば、この答えは関数カリー化ソリューションで予期しない動作に遭遇した他の誰かを助けるでしょう。
ここにいくつかの素晴らしい答えがあるにもかかわらず、ユーザーが入力した検索をデバウンスするための私のお気に入りの(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以外のプラットフォームでも安全に使用できます 。
私はこの古き良き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
}
この方法の大きな利点はパラメータを渡すことができる(ここでは検索文字列)です。ここに示されているほとんどのデバウンサーでは、そうではありません...
クラスを使用した別のデバウンス実装、あなたは役に立つかもしれません: https://github.com/webadnan/Swift-debouncer
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つだけが受け入れられ、前の要求はすべてキャンセルされます。単純に保つために、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()
quickthyme のいくつかの微妙な改善 優れた答え :
delay
パラメータを追加します。おそらくデフォルト値を使用します。Debounce
の代わりにenum
をclass
にすると、_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)
}
}
}
_
また、呼び出しサイトでジェネリック型を明示的に宣言する必要もありません—推測できます。たとえば、Debounce
とUISearchController
をupdateSearchResults(for:)
で使用する場合(UISearchResultsUpdating
の必須メソッド)、次のようにします。
_func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else { return }
Debounce.input(text, current: searchController.searchBar.text ?? "") {
// ...
}
}
_
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がフォーカスを失ってもタイマーはトリガーされません。