I'm working on a timer app for iPhone. But when switching views and coming back to initial timer view,
the label is not updated. While I can see it's still running in the print log.
I have the code below in my viewDidLoad.
How can I start refreshing the label again when I enter the timer view again?
The other view is handled through Segue.
func updateTime() {
var currentTime = NSDate.timeIntervalSinceReferenceDate()
//Find the difference between current time and start time.
var elapsedTime: NSTimeInterval = currentTime - startTime
//calculate the minutes in elapsed time.
let minutes = UInt8(elapsedTime / 60.0)
elapsedTime -= (NSTimeInterval(minutes) * 60)
//calculate the seconds in elapsed time.
let seconds = UInt8(elapsedTime)
elapsedTime -= NSTimeInterval(seconds)
//find out the fraction of milliseconds to be displayed.
let fraction = UInt8(elapsedTime * 100)
//add the leading zero for minutes, seconds and millseconds and store them as string constants
let strMinutes = minutes > 9 ? String(minutes):"0" + String(minutes)
let strSeconds = seconds > 9 ? String(seconds):"0" + String(seconds)
println("----------")
println("currentTime")
println (currentTime)
println("elapsedTime")
println (elapsedTime)
println("extraTime")
println (extraTime)
println("summed")
println (summed)
//concatenate minuets, seconds and milliseconds as assign it to the UILabel
displayTimeLabel.text = "\(strMinutes):\(strSeconds)"
}
#IBAction func start(sender: AnyObject) {
if (!timer.valid) {
let aSelector : Selector = "updateTime"
timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: aSelector, userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate()
}
}
#IBAction func stop(sender: AnyObject) {
timer.invalidate()
}
I was having the exact same problem in a tab view application and solved it using the NSNotification Center. To make it work in your case, you could make a separate function just for updating the text.
func updateText(notification: NSNotification) {
displayTimeLabel.text = "\(strMinutes):\(strSeconds)"
}
Then inside your "updateTime" function, where you had the line I took out, replace it with a postNotifiction:
NSNotificationCenter.defaultCenter().postNotificationName("UpdateTimer", object: nil)
Then put the observer inside a ViewDidAppear function in the View Controller where the text should be updated:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(false)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateText:", name: "UpdateTimer", object: nil)
}
With the observer in viewDidAppear, the updateText function always gets called, and the text is updated even when you switch views and come back.
Related
New guy here teaching myself Swift. Building my first personal app and have hit a wall after several searches on here, youtube, and google. This is my first time posting a question (as I've been able to find my other answers on here).
I'm having issues with my timer updating on the UILabel. I've managed to find code on older versions of swift that get the timer to run and count down. I then figured out how to break the seconds down to minutes and seconds.
But what I find is when I run the app, the timer shows "30:0" (a different issue I need to figure out) and never counts down. When I leave the page in the simulator and come back, it's only then that the UILabel updates.
I know viewdidload only loads upon the first moment the page opens. I'm having a hard time figuring out how to get the UILabel to update every time a second changes. I'm not sure what code to implement.
Thank you so much!
import UIKit
var timer = Timer()
var timerDuration: Int = 1800
// This converts my timeDuration from seconds to minutes and seconds.
func secondsToHoursMinutesSeconds (seconds : Int) -> (h: Int, m : Int, s : Int) {
return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
}
class lyricWriteViewController: UIViewController {
//This takes the function to convert minutes and seconds and accepts an input, which I've chosen the variable timeDuration (which is currently 1800 seconds.
var theTimer = (h: 0, m: 0, s: 0)
#IBOutlet weak var countdownTimer: UILabel!
#IBOutlet weak var randomLyric: UILabel!
#IBOutlet weak var titleInput: UITextField!
#IBOutlet weak var lyricInput: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
//This line takes a random array number and shows it on the textlabel.
randomLyric.text = oneLiner
theTimer = secondsToHoursMinutesSeconds(seconds: timerDuration)
//This is the code the does the counting down
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(lyricWriteViewController.counter), userInfo: nil, repeats: true)
}
#objc func counter() {
timerDuration -= 1
// Below is the timer view I've created. It has converted seconds to minutes and seconds but the screen won't refresh. Also, when the seconds number hits zero, it does "0" instead of "00".
let displayTimer = "\(theTimer.m) : \(theTimer.s)"
countdownTimer.text = String(displayTimer)
//When the timer hits 0, it stops working so that it doesn't go into negative numbers
if timerDuration == 0 {
timer.invalidate()
}
}
func submitlyricsButton(_ sender: UIButton) {
//I will eventually tie this to a completed lyric tableview.
}
}
var timer: Timer?
var totalTime = 120
private func startOtpTimer() {
self.totalTime = 120
self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
print(self.totalTime)
self.lblTimer.text = self.timeFormatted(self.totalTime) // will show timer
if totalTime != 0 {
totalTime -= 1 // decrease counter timer
}
else {
if let timer = self.timer {
timer.invalidate()
self.timer = nil
}
}
}
func timeFormatted(_ totalSeconds: Int) -> String {
let seconds: Int = totalSeconds % 60
let minutes: Int = (totalSeconds / 60) % 60
return String(format: "%02d:%02d", minutes, seconds)
}
It is because you are not updating the theTimer value. As viewDidLoad() is called once it is not working fine, you need to update theTimer value after deducting 1 from it.
So move this line :
theTimer = secondsToHoursMinutesSeconds(seconds: timerDuration)
in counter() funtion after timerDuration -= 1. So your function should look like this :
#objc func counter() {
timerDuration -= 1
if timerDuration == 0 {
timer.invalidate()
} else {
theTimer = secondsToHoursMinutesSeconds(seconds: timerDuration)
let displayTimer = "\(theTimer.m) : \(theTimer.s)"
countdownTimer.text = String(displayTimer)
}
}
Also move all of this inside controller:
var timer = Timer()
var timerDuration: Int = 1800
// This converts my timeDuration from seconds to minutes and seconds.
func secondsToHoursMinutesSeconds (seconds : Int) -> (h: Int, m : Int, s : Int){
return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)}
As timerDuration is global you will have to kill the app and run it again to see the timer working again.
Replace countdownTimer.text = String(displayTimer) with
DispatchQueue.main.async {
countdownTimer.text = String(displayTimer)
}
What I think is happening here is since countdownTimer.text = String(displayTimer) is not running on the main thread it is not updating immediately. It does however after a period of time (Like you said, when when you traverse the screen).
Sorry if this is a newbie question, I am very new to iOS & Swift. I have a problem with the timer interval: I set 0.01 time interval but it doesn't correspond with the timer label, because 0.01 corresponds in one millisecond but it doesn't show it. So basically the timer is skewed.
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(updateStopwatch) , userInfo: nil, repeats: true)
#IBAction func startStopButton(_ sender: Any) {
buttonTapped()
}
func updateStopwatch() {
milliseconds += 1
if milliseconds == 100 {
seconds += 1
milliseconds = 0
}
if seconds == 60 {
minutes += 1
seconds = 0
}
let millisecondsString = milliseconds > 9 ?"\(milliseconds)" : "0\(milliseconds)"
let secondsString = seconds > 9 ?"\(seconds)" : "0\(seconds)"
let minutesString = minutes > 9 ?"\(minutes)" : "0\(minutes)"
stopWatchString = "\(minutesString):\(secondsString).\(millisecondsString)"
labelTimer.text = stopWatchString
}
func buttonTapped() {
if isTimerRunning {
isTimerRunning = !isTimerRunning
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(updateStopwatch) , userInfo: nil, repeats: true)
startStopButton.setTitle("Stop", for: .normal)
}else{
isTimerRunning = !isTimerRunning
timer.invalidate()
startStopButton.setTitle("Start", for: .normal)
}
}
Devices have maximum screen update rate (most are 60 fps), so there is no point in going faster than that. For maximum screen refresh rate, use a CADisplayLink rather than a Timer, which is coordinated perfectly for screen refreshes (not only in frequency, but also the timing within the screen refresh cycle).
Also don't try to keep track of the time elapsed by adding some value (because you are not guaranteed that it will be called with the desired frequency). Instead, before you start your timer/displaylink, save the start time and then when the timer/displaylink is called, display the elapsed time in the desired format.
For example:
var startTime: CFTimeInterval!
weak var displayLink: CADisplayLink?
func startDisplayLink() {
self.displayLink?.invalidate() // stop prior display link, if any
startTime = CACurrentMediaTime()
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .current, forMode: .commonModes)
self.displayLink = displayLink
}
func handleDisplayLink(_ displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
let minutes = Int(elapsed / 60)
let seconds = elapsed - CFTimeInterval(minutes) * 60
let string = String(format: "%02d:%05.2f", minutes, seconds)
labelTimer.text = string
}
func stopDisplayLink() {
displayLink?.invalidate()
}
Note, CACurrentMediaTime() uses mach_time, like hotpaw2 correctly suggested, but does the conversion to seconds for you.
The time delay of a scheduledTimer is only approximate, and can differ from what is requested by many milliseconds, due to iOS overhead. A repeating Timer is even worse for timing, as any delay jitter errors will accumulate. So don't use a Timer for timing longer events.
A CADisplayLink is a more reliable timer, as it is synchronized to the 60 Hz display refresh (e.g. this is the maximum rate that any UILabel can be changed on devices other than the latest iPad Pros). There is no use trying to update a time display any faster (except possibly on the latest iPad Pros).
Also, do not use Date methods for timing, as they are not guaranteed to be monotonic when the device is connected to a network (as NTP can change the clock time right in the middle of your timing activity).
You should check any elapsed time measurement UI against one of the built-in timers such as mach_time. mach_absolute_time() is guaranteed to be monotonic, and not affected by NTP or other network activity.
I have created one timer object and set #selector method, In #selector method my label update every time that display timer count down value, but when I push or pop another view controller and come back to timer view controller my label not updating timer count down value
override func viewDidLoad() {
super.viewDidLoad()
if timer == nil {
self.startTimer()
}
}
func startTimer() {
timer?.invalidate()
timer = Timer.scheduledTimer(timeInterval:1, target: self, selector: #selector(self.update), userInfo: nil, repeats: true);
}
func update() {
count += 1
if(count > 0){
let ti = NSInteger(count)
let strSeconds = ti % 60
let strMinutes = (ti / 60) % 60
let strHours = (ti / 3600)
print("\(strHours):\(strMinutes):\(strSeconds)")
self.lblTimer.text = String(format: "%0.2d:%0.2d:%0.2d",strHours,strMinutes,strSeconds)
}
}
When you use the selector try self.update() to actually call the function. May also put a print statement to check that your variable is indeed incrementing.
I have a label and its value is "03:48".
I want to countdown it like a music player. How can I do that?
03:48 03:47 03:46 03:45 ... 00:00
var musictime =3:48
func stringFromTimeInterval(interval: NSTimeInterval) -> String {
let interval = Int(interval)
let seconds = interval % 60
let minutes = (interval / 60)
return String(format: "%02d:%02d", minutes, seconds)
}
func startTimer() {
var duration=musictime.componentsSeparatedByString(":") //split 3 and 48
var count = duration[0].toInt()! * 60 + duration[1].toInt()! //224 second
timerCounter = NSTimeInterval( count )
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "onTimer:", userInfo: nil, repeats: true)
}
#objc func onTimer(timer:NSTimer!) {
// Here is the string containing the timer
// Update your label here
//println(stringFromTimeInterval(timerCounter))
statusLabel.text=stringFromTimeInterval(timerCounter)
timerCounter!--
}
You should take a look at NSDate property timeIntervalSinceNow. All you need is to set a future date as the endDate using NSDate method dateByAddingTimeInterval as follow:
class ViewController: UIViewController {
#IBOutlet weak var timerLabel: UILabel!
var remainingTime: NSTimeInterval = 0
var endDate: NSDate!
var timer = NSTimer()
override func viewDidLoad() {
super.viewDidLoad()
remainingTime = 228.0 // choose as many seconds as you want (total time)
endDate = NSDate().dateByAddingTimeInterval(remainingTime) // set your future end date by adding the time for your timer
timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "updateLabel", userInfo: nil, repeats: true) // create a timer to update your label
}
func updateLabel() {
timerLabel.text = endDate.timeIntervalSinceNow.mmss
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
// you will need this extension to convert your time interval to a time string
extension NSTimeInterval {
var mmss: String {
return self < 0 ? "00:00" : String(format:"%02d:%02d", Int((self/60.0)%60), Int(self % 60))
}
var hmmss: String {
return String(format:"%d:%02d:%02d", Int(self/3600.0), Int(self / 60.0 % 60), Int(self % 60))
}
}
Well, by writing code. This is not a "Teach me how to program site. It's a site where you post questions about specific problems you are having with code you have written.
In short, though do the following:
Record your end time
let endInterval = NSDate.timeIntervalSinceReferenceDate() + secondsToEnd
Create a repeating timer that fires once/second.
Each time it fires, compute the number of seconds remaining to endInterval.
Calculate minutes remaining as
Int((endInterval-nowInterval)/60)
Calculate seconds remaining as
Int(endInterval-nowInterval)%60
There is also the new (to iOS 8) class NSDateComponentsFormatter, which I've read a little about but haven't used. I believe that will generate formatted timer intervals like hh:mm:ss for you automatically. You'd use the same approach I outlined above, but instead of calculating minutes and seconds yourself, use the NSDateComponentsFormatter.
I need to have a label countdown from 20secs to 0 and start over again. This is my first time doing a project in Swift and I am trying to use NSTimer.scheduledTimerWithTimeInterval. This countdown should run in a loop for a given amount of times.
I am having a hard time implementing a Start and Start again method (loop). I basically am not finding a way to start the clock for 20s and when it's over, start it again.
I'd appreciate any idea on how to do that
Wagner
#IBAction func startWorkout(sender: AnyObject) {
timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("countDownTime"), userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate()
}
func countDownTime() {
var currentTime = NSDate.timeIntervalSinceReferenceDate()
//Find the difference between current time and start time.
var elapsedTime: NSTimeInterval = currentTime - startTime
//calculate the seconds in elapsed time.
let seconds = UInt8(elapsedTime)
elapsedTime -= NSTimeInterval(seconds)
//find out the fraction of milliseconds to be displayed.
let fraction = UInt8(elapsedTime * 100)
//add the leading zero for minutes, seconds and millseconds and store them as string constants
let strSeconds = seconds > 9 ? String(seconds):"0" + String(seconds)
let strFraction = fraction > 9 ? String(fraction):"0" + String(fraction)
//concatenate minuets, seconds and milliseconds as assign it to the UILabel
timeLabel.text = "\(strSeconds):\(strFraction)"
}
You should set your date endTime 20s from now and just check the date timeIntervalSinceNow. Once the timeInterval reaches 0 you set it 20 seconds from now again
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var strTimer: UILabel!
var endTime = Date(timeIntervalSinceNow: 20)
var timer = Timer()
#objc func updateTimer(_ timer: Timer) {
let remaining = endTime.timeIntervalSinceNow
strTimer.text = remaining.time
if remaining <= 0 {
endTime += 20
}
}
override func viewDidLoad() {
super.viewDidLoad()
strTimer.font = .monospacedSystemFont(ofSize: 20, weight: .semibold)
strTimer.text = "20:00"
timer = .scheduledTimer(timeInterval: 1/30, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
}
extension TimeInterval {
var time: String {
String(format: "%02d:%02d", Int(truncatingRemainder(dividingBy: 60)), Int((self * 100).truncatingRemainder(dividingBy: 100)))
}
}
In your countdownTime(), change your startTime to current time when your elapsed time reaches 20 seconds
First of all, if you're simply looping without stopping, you could just use module to get the seconds. That is seconds % 20 would just jump from 19.9 to 0.0. And thus, if your counting down, you would calculate seconds - seconds % 20 which would jump to 20 when it reached zero. Over and over again. Is this what you're after?
For the leading zeros you can use: String(format: "%02d:%02d", seconds, fraction). Please note the formatting: here seconds and fraction are integer numbers.
But if you need to stop the timer, you have to keep track of the previously counted seconds and reset the startTime at each start. Every time you stop, you'd have to add up the current seconds to previously counted seconds. Am I making any sense?
To minimize processing, you could create two timers. One timer for 20 seconds and another timer for how often you want to update the UI. It would be difficult to see 100 frames per second. If you are checking every 0.01, your code is less accurate. The manual is really helpful. https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/ When you no longer have use of the timer invalidate and set to nil. Other timing functions exist also.