UITextField + Timer Issue - Checking for "Editing Changed" but Only Once to Initiate Timer?

Viewed 59

Full Disclosure: I am extremely new to everything swift/UIKit-related so I hope you can excuse my assured ignorance.


So the basic action I am trying to preform is having a Timer immediately begin a countdown the moment any character is entered into a UITextField (NOT triggered just by being focused).

Everything looks and functions 90% as I am intending except for an interaction where every time a character is entered into text field, the timer recursively initiates it's timeInterval.

After 7+ characters are typed, my 60-Second timer is far past 0 in less than a few seconds.

Using editingDidBegin "fixes" the issue but, in this parameter, the timer starts when the text field is tapped and focused and that isn't ideal for what I am trying to do.

My ViewController Code:

import UIKit

class SecondViewController: UIViewController, UITextFieldDelegate {
    var textField: UITextField?
    @IBOutlet weak var countDownLabel: UILabel!   
    var seconds = 60
    var timer = Timer()
    @IBAction func inputTextBar(_ sender: Any) {
        func inputTextBar() {
            self.becomeFirstResponder()
        }
        timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(SecondViewController.counterb), userInfo: nil, repeats: true)
    }
    @objc func counterb() {
        seconds -= 1
        countDownLabel.text = String(seconds)
        if seconds == 0 {
            timer.invalidate()
            countDownLabel.text = "Time's Up!"
        }
    }
    var count = 10
    override func viewDidLoad() {
        super.viewDidLoad()
        textField?.delegate = self
        textField?.becomeFirstResponder()
    }
}

Other Noted Confusion: If only one character is typed, the timer proceeds as normal and prints "Times Up!" at 0sec. and stops.

But if it's on the speed-pileup I mentioned, after a few characters are typed, the timer flies past 0 and never invalidates.

I assume that's a refresh issue.

Edit:

Okay, I figured out that making the timer variable weak I was able to get the timer to not keep stacking on itself. But, with the weak variable, the timer doesn't run at all while the UITextField is being typed in!

Semi-Fixed Code:

weak var timer: Timer?
@IBAction func inputTextBar(_ sender: AnyObject) {
    timer?.invalidate()      
    timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(SecondViewController.loop), userInfo: nil, repeats: true)
}    
@objc func loop() {
    seconds -= 1
    countDownLabel.text = String(seconds)   
    if self.seconds == 0 {
        timer?.invalidate()
        countDownLabel.text = "Time's Up!"
    }   
}
1 Answers

First change your timer declaration to optional. Then you can simply check if it is nil, if true schedule your timer. Second you should never rely on a timer to measure elapsed time. Just create an end date adding the desired time interval from now. This way you can simply check if now is greater or equal than the endDate when invalidating your timer. Use the timer only to update the UI and you can simply check the time interval since now when updating your label. The code below was written in the browser so it might have some mistakes but you can have an idea what to do to accomplish your goal.

class SecondViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var countDownLabel: UILabel!
    var endDate: Date? = nil
    var timer: Timer? = nil
    override func viewDidLoad() {
        super.viewDidLoad()
        textField.addTarget(self, action: #selector(editingChanged), for: .editingChanged)
        textField.becomeFirstResponder()
    }
    @objc func editingChanged(_ textField: UITextField) {
        if timer == nil {
            timer = .scheduledTimer(timeInterval: 1,
                                    target: self,
                                    selector: #selector(counterb),
                                    userInfo: nil,
                                    repeats: true)
            endDate = Date().addingTimeInterval(60)
        }
    }
    @objc func counterb() {
        countDownLabel.text = String(format: "%.0f", endDate!.timeIntervalSinceNow.rounded(.up))
        if Date() >= endDate! {
            timer?.invalidate()
            timer = nil
            endDate = nil
            countDownLabel.text = "Time's Up!"
        }
    }
}
Related