Как я могу отказаться от вызова метода?

Я пытаюсь использовать 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() несколько раз, и он будет сохраняться только один раз в секунду.

Ответ 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
    }

}