0

I'm trying to find the best way to handle this. I'm currently looking to wait until user stops typing (let's say delay 3s) before running some code. Initially I was going to use textFieldDidEndEditing but as we know this only executes when user returns, rather than when they stop typing.

Currently I use an @IBAction textFieldEditingChanged but this will execute the code with each entered character/number. I tried to use a variable like shouldExecuteCode: Bool but since I'm parsing an XML, the response is almost immediate and the code still get executed several times.

I was thinking to use some sort of OperationQueue with one operation or no concurrency.

Anyone got a good idea for this?

Current Implementation

 @IBAction func textFieldEditingChanged(_ sender: CurrencyTextField) {
        self.viewModel.currentTextFieldIdentifier = sender.textFieldIdentifier
        DispatchQueue.main.asyncAfter(deadline: .now() + viewModel.calculationDelay) {
            if let text = sender.text,
               let value = String.format(text) {
                switch self.viewModel.currentTextFieldIdentifier {
                case .fromCurrency:
                    self.calculateExchange(value: value,
                                           valueCurrency: self.fromCurrencyTextField.currency,
                                           outputCurrency: self.toCurrencyTextField.currency)
                case .toCurrency:
                    self.calculateExchange(value: value,
                                           valueCurrency: self.toCurrencyTextField.currency,
                                           outputCurrency: self.fromCurrencyTextField.currency)
                }
            }
        }
    }

Potential Implementation

NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: sender)
            .map({ ($0.object as? UITextField)?.text ?? "" })
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .sink { [weak self] text in
                guard let self = self  else { return }
                if let value = String.format(text) {
                    print(value)
                    switch self.viewModel.currencyTextFieldIdentifier {
                    case .fromCurrency:
                        self.calculateExchange(value: value,
                                               valueCurrency: self.fromCurrencyTextField.currency,
                                               outputCurrency: self.toCurrencyTextField.currency)
                    case .toCurrency:
                        self.calculateExchange(value: value,
                                               valueCurrency: self.toCurrencyTextField.currency,
                                               outputCurrency: self.fromCurrencyTextField.currency)
                    }
                }
            }
            .store(in: &subscriptions)
Ace Green
  • 381
  • 5
  • 29
  • Look into using the Combine framework. The docs have nice writeup [Receiving and Handling Events with Combine](https://developer.apple.com/documentation/combine/receiving-and-handling-events-with-combine) that even shows an example using `NSTextField`. Should be easy to adapt to `UITextField`. I believe the key to your requirement on the 3 second delay is to use `debounce` (which is also covered in that example). – HangarRash May 24 '23 at 20:37
  • @HangarRash Thanks for pointing me in the right direction. I implemented this but rather than getting executed with each character, now even one character triggers like 20 times?. Added updated code to question – Ace Green May 24 '23 at 21:02
  • @HangarRash is it because it's still within my @IBAction `textFieldEditingChanged`? If so, I need to know the `object` as I have two textFields that need this block of code to be executed – Ace Green May 24 '23 at 21:09
  • The use of Combine should replace your use of `textFieldEditingChanged`. – HangarRash May 24 '23 at 21:14
  • yea but then I need to setup a Notification for each textField, not really ideal. I'm better of setting up a `Timer` then or? – Ace Green May 24 '23 at 21:17
  • just for testing, I replaced `textFieldEditingChanged` with two of these notifications and still, it executes with every character – Ace Green May 24 '23 at 21:34
  • Change the debounce from 0.5 seconds to 3 seconds like you asked about. – HangarRash May 24 '23 at 21:36
  • @Rob I usually do that, check my last question, I just did that. I edited my question to show HangarRash the implementation I did, as I wasn't sure if it was correct and couldn't post it in the comments – Ace Green May 24 '23 at 23:39
  • @Rob Again I didn't post it as an answer, I posted it so he can see it because I wanted his input but couldn't post it in the comments. I explained to you that I usually post my answer and my implementation was based of the link and not of his answer, his answer came after because I had more questions. So no credit was required. It's getting annoying to have to justify myself. You jumped half way into the conversation and made assumptions. – Ace Green May 25 '23 at 00:43
  • You make too many assumptions. If you check my Double extension, it uses `NumberFormatter ` and I use a tag for very specific use cases, this is one of them, you don't know the rest of my code. Nothing wrong with using tags, its not outdated – Ace Green May 25 '23 at 00:50
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253812/discussion-between-rob-and-ace-green). – Rob May 25 '23 at 01:17
  • @Rob no need, as a matter of fact, I have addressed both your comments in my code already. The tag issue, required that I propagate my delegate to my second subclass, and I already had a String.format() method. This code was not meant for release, all this is besides the point of this question. This implementation was just a rough draft. If you want, I can put up a PR ‍♂️ – Ace Green May 25 '23 at 01:42
  • Just so we can end this, I updated the question. Sleep tight now – Ace Green May 25 '23 at 01:48

1 Answers1

1

Here is an example using the Combine framework. This sets up two simple UITextFields and sets up one sink for both such that you don't get any notifications until no change has occurred in either text field for 3 seconds.

class ViewController: UIViewController {
    var sub: AnyCancellable!

    override func viewDidLoad() {
        super.viewDidLoad()

        let tf1 = UITextField(frame: CGRect(x: 70, y: 70, width: 150, height: 44))
        tf1.borderStyle = .roundedRect
        self.view.addSubview(tf1)

        let tf2 = UITextField(frame: CGRect(x: 70, y: 120, width: 150, height: 44))
        tf2.borderStyle = .roundedRect
        self.view.addSubview(tf2)

        sub = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: nil)
            .debounce(for: .seconds(3), scheduler: RunLoop.main)
            .sink { _ in
                print("\(Date()): tf1: \(tf1.text!), tf2: \(tf2.text!)")
            }
    }
}

You can type all you want into a text field, switch to the other and type. The sink block will not run until you've stopped typing in both text fields for 3 seconds.

HangarRash
  • 7,314
  • 5
  • 5
  • 32
  • Oh interesting that you can use `object: nil` and it applies to both. I'll give It another try. Thanks for the help – Ace Green May 24 '23 at 23:38