8

I have UITextField for entering search string and UITableView with results. What i want is to run search function when user entered more than 3 letters and it at least 0,5 sec passed since last symbol added to UITextView.

I found (Detect when user stopped / paused typing in Swift) function and I added it to my ViewController that have class SearchVC and method server_search

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    NSObject.cancelPreviousPerformRequests(
        withTarget: self,
        selector: #selector(SearchVC.server_search),
        object: textField)
    self.perform(
        #selector(SearchVC.server_search),
        with: textField,
        afterDelay: 0.5)
    return true
}

But nothing happens.

Community
  • 1
  • 1
moonvader
  • 19,761
  • 18
  • 67
  • 116

3 Answers3

18

You can achieve this with a Timer

var timer: Timer?

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

    timer?.invalidate()  // Cancel any previous timer

    // If the textField contains at least 3 characters…
    let currentText = textField.text ?? ""
    if (currentText as NSString).replacingCharacters(in: range, with: string).characters.count >= 3 {

         // …schedule a timer for 0.5 seconds
         timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(performSearch()), userInfo: nil, repeats: false)
    }

    return true
}

func performSearch() {

} 

And don't forget to set the view controller to be your UITextField's delegate

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
14

While using a Timer is still a valid answer, since Swift 3, you can use a DispatchWorkItem

var workItem: DispatchWorkItem?

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

    // Cancel any outstanding search
    self.workItem?.cancel() 

    guard let text = textField.text, let textRange = Range(range, in: text) else {
        return true
    }

    let updatedText = text.replacingCharacters(in: textRange, with: string)

    guard updatedText.count >= 3 else { 
        return true
    }

    // Set up a DispatchWorkItem to perform the search
    let workItem = DispatchWorkItem { [weak self] in
        self?.performSearch(updatedText)
    }

    // Run this block after 0.5 seconds
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: workItem)

    // Keep a reference to it so it can be cancelled
    self.workItem = workItem 

    return true   
}

func performSearch(_ text: String) {

} 
Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
5

The use of Timers have some advantages. With your implementation you would cancel all the performs in for your object, which is something that may go out of control.

A Timer, instead, lets you fine grain control it. See the following implementation:

var searchTimer: Timer?
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    // Invalidate remove the previous timer, since it's optional if the previous timer is already cancelled or nil, it won't affect execution
    searchTimer?.invalidate()
    searchTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { (timer) in
        //do Something crazy
        self.server_search()
    })
    return true
}
Andre
  • 1,135
  • 9
  • 20
  • 1
    Thank your for answer - it seems to be fine but I selected answer of Ashley Mills as correct because it has string length checking and don't need iOS 10 – moonvader Apr 11 '17 at 11:35