Как я могу отказаться от вызова метода?
Я пытаюсь использовать UISearchView
для запроса мест Google. При этом при изменении текста вызывается мой UISearchBar
, я делаю запрос в google-места. Проблема в том, что я бы скорее отказался от этого вызова, чтобы запросить только один раз за 250 мс, чтобы избежать ненужного сетевого трафика. Я бы предпочел не писать эту функцию самостоятельно, но я буду, если мне нужно.
Я нашел: 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.
Ответы
Ответ 1
Поместите это на верхний уровень вашего файла, чтобы не путать себя с правилами смежных параметров 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
не прошел с момента последнего его вызова.
Ответ 2
версия Swift 3
1. Основная функция debounce
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. Параметрированная функция debounce
Иногда полезно, чтобы функция debounce принимала параметр.
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. Пример
В следующем примере вы можете увидеть, как работает debouncing, используя строковый параметр для идентификации вызовов.
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
Ответ 3
Для меня работает следующее:
Добавьте ниже к некоторому файлу в вашем проекте (я поддерживаю файл 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()
несколько раз, и он будет сохраняться только один раз в секунду.
Ответ 4
Другая реализация debounce, использующая класс, может оказаться полезной:
https://github.com/webadnan/swift-debouncer
Ответ 5
Я использовал этот старый добрый метод 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
}
Большим преимуществом этого метода является то, что я могу передавать параметры (здесь: строка поиска). В большинстве представленных Debouncers это не так...
Ответ 6
Сначала создайте общий класс 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, который использует механизм debounce:
//
// 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. Вы не должны устанавливать этот UIViewController подкласс как SearchBar делегат! Не используйте функции делегата. Вместо этого используйте предоставленные затворы!
Ответ 7
Общее решение, заданное вопросом и построенное на нескольких ответах, имеет логическую ошибку, которая вызывает проблемы с короткими порогами дебюта.
Начиная с предоставленной реализации:
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()
будет равно deadline
переданному asyncAfter(deadline: DispatchTime)
. Цель сравнения now.rawValue >= when.rawValue
- фактически сравнить ожидаемый срок с "самым последним" сроком. С небольшими порогами asyncAfter
, задержка 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 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
}
}
Надеюсь, этот ответ поможет кому-то другому, столкнувшись с неожиданным поведением с решением каррирования функций.
Ответ 8
Решение owenoak работает для меня. Я изменил его немного, чтобы соответствовать моему проекту:
Я создал быстрый файл 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)
}
}
Затем я добавил следующее в свой класс пользовательского интерфейса:
class func changed() {
print("changed")
}
let debouncedChanged = debounce(0.5, action: MainWindowController.changed)
Ключевым отличием от owenoak anwer является эта строка:
NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSEventTrackingRunLoopMode)
Без этой строки таймер не запускается, если пользовательский интерфейс теряет фокус.
Ответ 9
Вот реализация debounce для 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
}
}