How can I debounce a method call?

前端 未结 13 1455
醉梦人生
醉梦人生 2020-11-30 01:18

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

相关标签:
13条回答
  • 2020-11-30 01:46

    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!

    0 讨论(0)
  • 2020-11-30 01:46

    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.

    0 讨论(0)
  • 2020-11-30 01:50

    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...") 
    }
    
    0 讨论(0)
  • 2020-11-30 01:51

    A couple subtle improvements on quickthyme's excellent answer:

    1. Add a delay parameter, perhaps with a default value.
    2. Make 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 ?? "") {
            // ...
        }
    
    }
    
    0 讨论(0)
  • 2020-11-30 01:57

    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

    0 讨论(0)
  • 2020-11-30 02:00

    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
        }
    
    }
    
    0 讨论(0)
提交回复
热议问题