I\'m trying to use a UISearchView
to query google places. In doing so, on text change calls for my UISearchBar
, I\'m making a request to google pla
First, create a Debouncer generic class:
//
// 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)
}
}
Then create a subclass of UISearchBar that uses the debounce mechanism:
//
// 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 ?? "")
}
}
}
}
Note that this class is set as the UISearchBarDelegate. Actions will be passed to this class as closures.
Finally, you can use it like so:
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
}
}
Note that in that case, the DebounceSearchBar is already the searchBar delegate. You should NOT set this UIViewController subclass as the searchBar delegate! Nor use delegate functions. Use the provided closures instead!
owenoak's solution works for me. I changed it a little bit to fit my project:
I created a swift file 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)
}
}
Then I added the following in my UI class:
class func changed() {
print("changed")
}
let debouncedChanged = debounce(0.5, action: MainWindowController.changed)
The key difference from owenoak's anwer is this line:
NSRunLoop.currentRunLoop().addTimer(timer!, forMode: NSEventTrackingRunLoopMode)
Without this line, the timer never triggers if the UI loses focus.
Here's an option for those not wanting to create classes/extensions:
Somewhere in your code:
var debounce_timer:Timer?
And in places you want to do the debounce:
debounce_timer?.invalidate()
debounce_timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
print ("Debounce this...")
}
A couple subtle improvements on quickthyme's excellent answer:
delay
parameter, perhaps with a default value.Debounce
an enum
instead of a class
, so you can skip having to declare a 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)
}
}
}
It's also not necessary to explicitly declare the generic type at the call site — it can be inferred. For example, if you want to use Debounce
with a UISearchController
, in updateSearchResults(for:)
(required method of UISearchResultsUpdating
), you would do this:
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else { return }
Debounce.input(text, current: searchController.searchBar.text ?? "") {
// ...
}
}
Scenario: User taps on button continuously but only last one is accepted and all previous request is cancelled.To keep it simple fetchMethod() prints the counter value.
1: Using Perform Selector After a delay:
working example 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:Using 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()
refrence link
Here is a debounce implementation for 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
}
}