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.
Related
I am working on meditation app. In this app i have some musical content and some silent meditation section using timer. Timer is working fine when it is in foreground but it is running for only 3 min in background(when device is locked or user press home button to exit from the app). I am using swift4. What i have tried :
var timer: Timer!
var timeCounter : Int!
var backgroundTaskIdentifier: UIBackgroundTaskIdentifier?
var backgroundTask = BackgroundTask()
#IBOutlet weak var timeLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(self.backgroundTaskIdentifier!)
})
backgroundTask.startBackgroundTask()
DispatchQueue.global(qos: .background).async {
let currentRunLoop = RunLoop.current
let timeInterval = 1.0
self.timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(self.updateTimer), userInfo: nil, repeats: true)
self.timer.tolerance = timeInterval * 0.1
currentRunLoop.add(self.timer, forMode: .commonModes)
currentRunLoop.run()
}
}
}
#objc func updateTimer () {
timeCounter = timeCounter - 1
let minutes = Int(timeCounter) / 60 % 60
let seconds = Int(timeCounter) % 60
print("timeCounter", timeCounter)
if (timeCounter == 0){
if self.timer != nil {
self.timer?.invalidate()
self.timer = nil
player.play()
}
}
timeLabel.fadeTransition(0.4)
timeLabel.text = String(format: "%02i:%02i",minutes,seconds)
}
Thanks in Advance.
In general you get limited time from the OS to run in background. You can check and react to the background time left using:
UIApplication.shared.backgroundTimeRemaining
If conditions are good (device is unlocked, battery full ...) you typically get about 160-180 seconds.
You find detailed information in Apples documentation.
As you want to play audio, you can use "Plays Audio" background mode to not get cut by the OS:
Depending how you play audio, configuring the AudioSession might also improve things.
Edit:
How I understand now from your comment, you want your app to do something every 4 minutes. The only possiblility I see is to use the BackgroundFetch feature. This does not guarantee a fixed interval though.
I have a countdown timer to a specific date. It looks pretty good, and updates each second to show the countdown. Here is the code I'm using to create the timer:
func scheduleTimeLabelsUpdateTimer() {
var components = Calendar.current.dateComponents([.day, .hour, .minute, .second], from: Date())
components.second! += 1
let nextSecondDate = Calendar.current.date(from: components)!
let timer = Timer(fireAt: nextSecondDate, interval: 1, target: self, selector: #selector(updateTimeLabels), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: .commonModes)
}
However, I'd like it to update each second, on the second, so that the clock updates at the same time as each second passes by. Right now it updates each second from when this method is called, which is in viewDidLoad().
For example if the countdown is set for midnight, I want it to hit zero exactly at midnight. Right now it may hit zero slightly after midnight, depending on how far into the second it was when the user opened this screen.
EDIT: This is how the countdown is displayed to the user. updateTimeLabels() just sets the text for each of those labels based on the amount of time left until that date. I would like each of the labels to be updated exactly on each second. This way the countdown will "hit zero" exactly on time. Notice how right now, the number of seconds hits zero, and then the system clock on the status bar updates. I would like these to happen at the same time.
This code, which I found somewhere on Stack Overflow many months ago, is being called in updateTimeLabels() to calculate the remaining time:
public func timeOffset(from date: Date) -> (days: Int, hours: Int, minutes: Int, seconds: Int) {
// Number of seconds between times
var delta = Double(self.seconds(from: date))
// Calculate and subtract whole days
let days = floor(delta / 86400)
delta -= days * 86400
// Caluclate and subtract whole hours
let hours = floor(delta / 3600).truncatingRemainder(dividingBy: 24)
delta -= hours * 3600
// Calculate and subtract whole minutes
let minutes = floor(delta / 60.0).truncatingRemainder(dividingBy: 60)
delta -= minutes * 60
// What's left is seconds
let seconds = delta.truncatingRemainder(dividingBy: 60)
return (Int(days), Int(hours), Int(minutes), Int(seconds))
}
In your code, you never use nanosecond when you init your component
So the timer will always hit at the round second number.
I test your code below print the Date() in the selector
I'm not sure if this is what you are talking about "hit zero", hope this would help
Since a Timer is scheduled on the run loop and other things are happening in the run loop, it isn't particularly accurate; it will fire after the specified interval has passed, but not necessarily exactly when the interval has passed.
Rather than trying to schedule the timer for the desired time, you should run your timer faster than 1 second, say at 0.5 seconds and update your time remaining label each time the timer fires. This will give a smoother update to the display:
var timer: Timer?
func scheduleTimeLabelsUpdateTimer() {
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { (timer) in
self.updateTimeLabels()
}
}
UPDATE
Don't do all of that math; iOS has nice libraries built in to do all of this for you; Simply create your target date and use DateComponents to work it out for you.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
#IBOutlet weak var countDownLabel: UILabel!
let dateFormatter = DateFormatter()
var targetTime: Date!
var calendar = Calendar.current
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var components = DateComponents()
components.setValue(3, for: .month)
components.setValue(3, for: .day)
components.setValue(2017, for: .year)
components.setValue(0, for: .hour)
components.setValue(0, for: .minute)
components.setValue(1, for: .second)
self.targetTime = calendar.date(from: components)
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { (timer) in
self.updateLabel()
})
self.dateFormatter.timeStyle = .long
self.dateFormatter.dateStyle = .short
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func updateLabel() {
let now = Date()
self.label.text = self.dateFormatter.string(from: now)
let components = calendar.dateComponents([.day,.hour,.minute,.second], from: now, to: self.targetTime)
self.countDownLabel.text = String(format: "%d days %d hours, %d minutes %d seconds", components.day!, components.hour!, components.minute!,components.second!)
}
}
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.
I am trying to create a timer to countdown x minutes and y seconds.
I am computing the number of seconds and creating the InterfaceTimer like this:
timer.setDate(NSDate(timeIntervalSinceNow:Double(secondsValue+1)))
timer.stop()
after that I keep stoping it and starting it again and again, but the values are suddenly decreasing as "time(now) doesn't stop".
Eg: if the timer shows :55, I start it for 3sec and stop it, it shows :52, I wait 10seconds and then start it again, it starts from :42.
I can not save the value currently in the WKInterfaceTimer, so that I could start again from the same point. Everything I tried doesn't work. Did anyone work with the timer and it stayed at the same value after stopping it?
Yes the watchkit timer is a bit...awkward...and definitely not very intuitive. But that's just my opinion
You'll have to keep setting the date/timer each time the user chooses to resume the timer.
Remember, you'll also need an internal NSTimer to keep track of things since the current WatchKit timer is simply for display without having any real logic attached to it.
So maybe something like this...It's not elegant. But it works
#IBOutlet weak var WKTimer: WKInterfaceTimer! //watchkit timer that the user will see
var myTimer : NSTimer? //internal timer to keep track
var isPaused = false //flag to determine if it is paused or not
var elapsedTime : NSTimeInterval = 0.0 //time that has passed between pause/resume
var startTime = NSDate()
var duration : NSTimeInterval = 45.0 //arbitrary number. 45 seconds
override func willActivate(){
super.willActivate()
myTimer = NSTimer.scheduledTimerWithTimeInterval(duration, target: self, selector: Selector("timerDone"), userInfo: nil, repeats: false)
WKTimer.setDate(NSDate(timeIntervalSinceNow: duration ))
WKTimer.start()
}
#IBAction func pauseResumePressed() {
//timer is paused. so unpause it and resume countdown
if isPaused{
isPaused = false
myTimer = NSTimer.scheduledTimerWithTimeInterval(duration - elapsedTime, target: self, selector: Selector("timerDone"), userInfo: nil, repeats: false)
WKTimer.setDate(NSDate(timeIntervalSinceNow: duration - elapsedTime))
WKTimer.start()
startTime = NSDate()
pauseResumeButton.setTitle("Pause")
}
//pause the timer
else{
isPaused = true
//get how much time has passed before they paused it
let paused = NSDate()
elapsedTime += paused.timeIntervalSinceDate(startTime)
//stop watchkit timer on the screen
WKTimer.stop()
//stop the ticking of the internal timer
myTimer!.invalidate()
//do whatever UI changes you need to
pauseResumeButton.setTitle("Resume")
}
}
func timerDone(){
//timer done counting down
}
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.