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

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!"
}
}

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

Repeating timer with New Time Intervals

Hi this is my first app and I have been trying to figure out how to do this simple task but am getting overloaded with information and I have a feeling there may be an easier way to do it.
I am trying to build a random word generator that will select from an array of words at random time intervals. The time needs to change after the timer fires. The timer can keep running the whole time the app is on.
here is my code so far:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
#objc weak var paraTimer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
let randomNum = Int.random(in: 1..<5)
paraTimer = Timer.scheduledTimer(timeInterval: TimeInterval(randomNum), target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true)
}
/*
This invalidates my timer, not sure if I need to start a new timer now or if I can reset this?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
paraTimer?.invalidate()
*/
#objc func runTimedCode() {
let array = ["Apple", "Banana", "Pear" ]
label.text = array.randomElement()
}
}
So the issue that I need to fix is it will select a random number from 1-5 and then not select a new number and just keep running every x seconds. Also should I invalidate the timer or leave that off?
Thank you. Like I said, this is my first time working on an app that doesn't just follow a straight tutorial.
To restart a timer with different intervals you have to recreate the timer.
First of all declare the timer strong and #objc is not needed
var paraTimer: Timer?
I recommended to create an extra method. Declare the timer non-repeating and use the block based API of Timer. The timer restarts itself.
func restartTimer() {
let randomNum = Int.random(in: 1..<5)
paraTimer = Timer.scheduledTimer(timeInterval: TimeInterval(randomNum), repeats: false) { [weak self] _ in
let array = ["Apple", "Banana", "Pear" ]
self?.label.text = array.randomElement()
self?.restartTimer()
}
}
Start the timer in viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
restartTimer()
}
and stop it in viewDidDisappear
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
paraTimer?.invalidate()
paraTimer = nil
}

Convert seconds to Minutes:Seconds in label MacOS

I am a school teacher and we have been 'ordered' to use a specific timer for our lessons, which low and behold doesn't work on our apple iMac's. I am trying to create my own in xcode and so far have created a basic window which will countdown a label in seconds. I have at the moment assigned the buttons and they work (in increments of 60 seconds).
This works and is fine but ideally i would like the label to display minutes and seconds instead (much easier for the kids). What is the best way to code this? Last time i used xcode was in 2009 and i am way out of date now!! Thanks in advance
--
#objc func updateTimer() {
seconds -= 1 //This will decrement(count down)the seconds.
countdownLabel.stringValue = "\(seconds)" //This will update the label.
}
--
#objc func runTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector:(#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
}
--
#IBAction func threeMin(_ sender: Any) {
seconds = 180
runTimer()
}
--
There are many solutions. A convenient one is DateComponentsFormatter
let formatter : DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
return formatter
}()
#objc func updateTimer() {
seconds -= 1
countdownLabel.stringValue = formatter.string(from: TimeInterval(seconds))!
}
Some improvements:
Assign tags to all buttons with their value in seconds for example set the tag of the threeMin button to 180. Then use only one IBAction and connect all buttons to that action.
In the action first check if the timer is running and start it only if it's not running
var timer : Timer?
#IBAction func startTimer(_ sender: NSButton) {
if timer == nil {
seconds = sender.tag
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController.updateTimer), userInfo: nil, repeats: true)
}
}
Create a function to stop the timer reliably
func stopTimer() {
if timer != nil {
timer?.invalidate()
timer = nil
}
}
In the updateTimer() function stop the timer if seconds is 0
#objc func updateTimer() {
seconds -= 1
countdownLabel.stringValue = formatter.string(from: TimeInterval(seconds))!
if seconds == 0 { stopTimer() }
}

How to count Timer on Swift

I'm a newbie in swift and I tried to make a Timer. It should normally count the seconds and print them in the debugger.
I tried this:
var timer = Timer()
#IBAction func killTimer(_ sender: AnyObject) {
timer.invalidate()
}
#objc func processTimer() {
print("This is a second")
}
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController.processTimer), userInfo: nil, repeats: true)
}
I don't know how the timer count seconds.. With this code i get an fail message:
#objc func processTimer() {
print("This is second \(Timer + 1)")
}
Thanks for your help.
A
You need a counter variable which is incremented every time the timer fires.
Declare the variable Timer as optional to invalidate the timer reliably (only once).
var timer : Timer?
var counter = 0
#IBAction func killTimer(_ sender: AnyObject) {
timer?.invalidate()
timer = nil
}
#objc func prozessTimer() {
counter += 1
print("This is a second ", counter)
}
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(timeInterval:1, target:self, selector:#selector(prozessTimer), userInfo: nil, repeats: true)
}
You need to start your timer, you have only initialised it in your code so in viewDidLoad add
timer.fire()
I am not sure what you want to print but if it is a timestamp then you could do add a property to your class
let formatter = DateFormatter()
and configure it to show time in seconds and milliseconds
formatter.dateFormat = "ss.SSS" //in viewDidLoad
and use it in your print statement
print("This is a second \(formatter.string(from(Date())")
If you want to print the time that passed use this (If that's what you want):
print("time passed: \(Date().timeIntervalSince1970 - timer.fireDate.timeIntervalSince1970)")

Having UILabel update based on Audio Player current time

I am attempting to have my UILabel update based on the duration of what the AudioPlayer current time is. I don't want to use a timer, because within my app people will be recording themselves with small durations & the half seconds are important, so I wouldn't really want to use timer(). Instead what I am attempting to do is just have the label update based on what the current time is automatically. I can't however seem to figure it out...
Here is how I am going about it, but clearly I am not doing it correctly
var audioPlayer : AVAudioPlayer!
var audioTime = UILabel() {
didSet {
do {
try audioPlayer = AVAudioPlayer(contentsOf: audioURL!)
let currentTime = Int(audioPlayer.currentTime)
let minutes = currentTime/60
let seconds = currentTime - minutes * 60
audioTime.text = (NSString(format: "%02d:%02d", minutes,seconds) as String)
} catch {
}
}
}
This might help you! Connect Outlet to showTimeLbl and try.
class ViewController: UIViewController {
#IBOutlet weak var showTimeLbl: UILabel!
var timeCount:Int = 0
var timer:Timer!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if(timer != nil)
{
timer.invalidate()
}
timer = Timer(timeInterval: 1.0, target: self, selector: #selector(ViewController.timerDidFire), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: RunLoopMode.commonModes)
}
func timerDidFire()
{
timeCount += 1
showTimeLbl.text = NSString(format: "%02d:%02d:%02d", timeCount/3600,(timeCount/60)%60,timeCount%60) as String
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
timer?.invalidate()
timer = nil
}

Swift 3 UIButton multiple events not detected within a less than 16 millisecond window

This is my first question on StackOverflow. I am trying to measure reaction time to stimuli and need multiple players and UIButtons for the players. The problem is if the two players in the code example are within 16 millisecond or less of each other both UIButton events come together and therefore the same time. I just did this in Android and had no problem.
I did try override of the touches_began() and the same problem. It seems as if the two UIButtons are being handled in the same interrupt as if they were a multi-touch which I have turned off for the two buttons.
I know the example is dumb but it does illustrate the 16 millisecond
issue. Just tap a bunch of times and see how many ties occur. I looked at the results of testing and I got an approximate 16 millisecond issue.
import UIKit
class ViewController: UIViewController {
weak var timer: Timer?
var startTime: Double = 0
var time: Double = 0
#IBOutlet weak var timeValueLabel1: UILabel!
#IBOutlet weak var timeValueLabel: UILabel!
#IBAction func stop1Button() {
let timeString = String(format: "%.3f", time)
timeValueLabel1.text = timeString
}
#IBAction func stopButton() {
let timeString = String(format: "%.3f", time)
timeValueLabel.text = timeString
}
override func viewDidAppear(_ animated: Bool) {
startTime = Date().timeIntervalSinceReferenceDate
timer = Timer.scheduledTimer(timeInterval: 0.001,
target: self,
selector: #selector(advanceTimer(timer:)),
userInfo: nil,
repeats: true)
}
override func viewWillDisappear(_ animated: Bool) {
timer?.invalidate()
}
func advanceTimer(timer: Timer) {
time = Date().timeIntervalSinceReferenceDate - startTime
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}