I'm working with timer and I've try to make it works in background. On simulator this works fine but on my device (iOS 11) it's very slow: 1 seconde became 5 or 6 secondes...
This is my code for run application in background:
backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(
expirationHandler:
{UIApplication.shared.endBackgroundTask(self.backgroundTaskIdentifier!)}
)
Info.plist:
Application does not run in background : NO
How can I make it works?
EDIT:
This is my timer code:
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self,
selector:#selector(ViewController.updateTimer), userInfo: nil, repeats: true)
EDIT 2:
func updateTimer () {
var j = 0
for _ in rows {
if (rows[j]["Playing"] as! Bool == true ) {
rows[j]["time"] = (rows[j]["time"] as! Double + 0.01) as AnyObject
// print(rows[j]["time"]) - PRINT OUTPUT HERE
rows[j]["lastTime"] = (rows[j]["lastTime"] as! Double + 0.01) as AnyObject
}
if (rows[j]["lastTime"] as! Double > 60.0) {
min[j] += 1
rows[j]["lastTime"] = 0.00 as AnyObject
}
j += 1
}
}
Instead of just printing your output, which might be executed from a different thread, print it from the main thread. That's why you might see the delay:
DispatchQueue.main.async {
print(rows[j]["time"])
}
Your timer interval is too short. Timers have a resolution of 50-100 ms, so your interval (10 ms) can't get executed so fast. I think when your app is in the background this effect increases.
Take a look at this answer: https://stackoverflow.com/a/30983444/5613280
Related
I'm still pretty new to coding and Swift. So bear with me.
Problem Statement : I've got a stopwatch style app that has two concurrent timers start at the same time and display in a mm:ss.SS format, but one is designed to reset to 0 at specific intervals automatically while the other keeps going and tracks total time.
Similar to a "lap" function but it does it automatically. The problem I've encountered is that occasionally the timers aren't perfectly synced up when the user pauses the timers. Since the reset happens at an exact second, both timers should have identical hundredths of a second, while the seconds and minutes will obviously be different. But sometimes the hundredths will be off by .01 or more.
Now, I know Timer isn't designed to be perfectly accurate, and in practice on my app this isn't even a huge deal. My timer doesn't even need to be accurate to the hundredth of a second, and while running it's not noticeably off at all, only while paused. I could display fewer decimal places or none at all, but I prefer the style of showing the hundredths since it fits in well with the stock timer app style.
So if there's a way to make this work, I'd like to keep it.
Screenshot : screenshot
What I tried :
#IBAction func playPauseTapped(_ sender: Any) {
if timerState == .new {
//start new timer
startCurrentTimer()
startTotalTimer()
currentStartTime = Date.timeIntervalSinceReferenceDate
totalStartTime = Date.timeIntervalSinceReferenceDate
timerState = .running
//some ui updates
} else if timerState == .running {
//pause timer
totalTimer.invalidate()
currentTimer.invalidate()
timerState = .paused
pausedTime = Date()
//other ui updates
} else if timerState == .paused {
//resume paused timer
let pausedInterval = Date().timeIntervalSince(pausedTime!)
pausedIntervals.append(pausedInterval)
pausedIntervalsCurrent.append(pausedInterval)
pausedTime = nil
startCurrentTimer()
startTotalTimer()
timerState = .running
//other ui updates
}
}
func startTotalTimer() {
totalTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(runTotalTimer), userInfo: nil, repeats: true)
}
func startCurrentTimer() {
currentTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(runCurrentTimer), userInfo: nil, repeats: true)
}
func resetCurrentTimer() {
currentTimer.invalidate()
currentStartTime = Date.timeIntervalSinceReferenceDate
pausedIntervalsCurrent.removeAll()
startCurrentTimer()
}
#objc func runCurrentTimer() {
let currentTime = Date.timeIntervalSinceReferenceDate
//calculate total paused time
var pausedSeconds = pausedIntervalsCurrent.reduce(0) { $0 + $1 }
if let pausedTime = pausedTime {
pausedSeconds += Date().timeIntervalSince(pausedTime)
}
let currentElapsedTime: TimeInterval = currentTime - currentStartTime - pausedSeconds
currentStepTimeLabel.text = format(time: currentElapsedTime)
if currentElapsedTime >= recipeInterval {
if recipeIndex < recipeTime.count - 1 {
recipeIndex += 1
//ui updates
//reset timer to 0
resetCurrentTimer()
} else {
//last step
currentTimer.invalidate()
}
}
}
#objc func runTotalTimer() {
let currentTime = Date.timeIntervalSinceReferenceDate
//calculate total paused time
var pausedSeconds = pausedIntervals.reduce(0) { $0 + $1 }
if let pausedTime = pausedTime {
pausedSeconds += Date().timeIntervalSince(pausedTime)
}
let totalElapsedTime: TimeInterval = currentTime - totalStartTime - pausedSeconds
totalTimeLabel.text = format(time: totalElapsedTime)
if totalElapsedTime >= recipeTotalTime {
totalTimer.invalidate()
currentTimer.invalidate()
//ui updates
}
}
func format(time: TimeInterval) -> String {
//formats TimeInterval into mm:ss.SS
let formater = DateFormatter()
formater.dateFormat = "mm:ss.SS"
let date = Date(timeIntervalSinceReferenceDate: time)
return formater.string(from: date)
}
You should use a single timer. And when you need a reset to zero, save the current time to a variable.
When presenting the time in the UI, calculate the difference between the running total timer, and the time you saved previously.
I have an app that does a countdown with a Timer. The countdown tracks multiple steps (all at the same intervals) as well as the total time left, and updates 2 separate UILabels accordingly. Occasionally, the labels will be out of sync.
I can't say for sure, but I think it might be only happening when I pause the countdown sometimes, and usually on steps later than the first step. It's most apparent on the last step when the two labels should be displaying the same exact thing, but will sometimes be 1 second off.
The other tricky thing is that sometimes pausing and resuming after the time has gone out of sync will get it back in sync.
My guess is I'm getting something weird happening in the pause code and/or the moving between steps, or maybe the calculating and formatting of TimeIntervals. Also I'm using rounded() on the calculated TimeIntervals because I noticed only updating the timer every 1s the labels would freeze and skip seconds a lot. But I'm unsure if that's the best way to solve this problem.
Here's the relevant code. (still need to work on refactoring but hopefully it's easy to follow, I'm still a beginner)
#IBAction func playPauseTapped(_ sender: Any) {
if timerState == .running {
//pause timer
pauseAnimation()
timer.invalidate()
timerState = .paused
pausedTime = Date()
playPauseButton.setImage(UIImage(systemName: "play.circle"), for: .normal)
} else if timerState == .paused {
//resume paused timer
guard let pause = pausedTime else { return }
let pausedInterval = Date().timeIntervalSince(pause)
startTime = startTime?.addingTimeInterval(pausedInterval)
endTime = endTime?.addingTimeInterval(pausedInterval)
currentStepEndTime = currentStepEndTime?.addingTimeInterval(pausedInterval)
pausedTime = nil
startTimer()
resumeAnimation()
timerState = .running
playPauseButton.setImage(UIImage(systemName: "pause.circle"), for: .normal)
} else {
//first run of brand new timer
startTimer()
startProgressBar()
startTime = Date()
if let totalTime = totalTime {
endTime = startTime?.addingTimeInterval(totalTime)
}
currentStepEndTime = Date().addingTimeInterval(recipeInterval)
timerState = .running
playPauseButton.setImage(UIImage(systemName: "pause.circle"), for: .normal)
currentWater += recipeWater[recipeIndex]
currentWeightLabel.text = "\(currentWater)g"
}
}
func startTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runTimer), userInfo: nil, repeats: true)
}
#objc func runTimer() {
let currentTime = Date()
guard let totalTimeLeft = endTime?.timeIntervalSince(currentTime).rounded() else { return }
guard let currentInterval = currentStepEndTime?.timeIntervalSince(currentTime).rounded() else { return }
//end of current step
if currentInterval <= 0 {
//check if end of recipe
if recipeIndex < recipeWater.count - 1 {
//move to next step
totalTimeLabel.text = totalTimeLeft.stringFromTimeInterval()
currentStepEndTime = Date().addingTimeInterval(recipeInterval)
startProgressBar()
currentStepTimeLabel.text = recipeInterval.stringFromTimeInterval()
stepsTime += recipeInterval
recipeIndex += 1
//update some ui
} else {
//last step
currentStepTimeLabel.text = "00:00"
totalTimeLabel.text = "00:00"
timer.invalidate()
//alert controller saying finished
}
} else {
//update time labels
currentStepTimeLabel.text = currentInterval.stringFromTimeInterval()
totalTimeLabel.text = totalTimeLeft.stringFromTimeInterval()
}
}
extension TimeInterval {
func stringFromTimeInterval() -> String {
let time = NSInteger(self)
let seconds = time % 60
let minutes = (time / 60) % 60
return String(format: "%0.2d:%0.2d",minutes,seconds)
}
}
EDIT UPDATE: I tried a few different things but still kept having the same issue. I started testing with printing the TimeInterval and the formatted string to compare and see what's off. It's definitely some sort of rounding error.
Total - 173.50678288936615 / 02:54
Step - 39.00026595592499 / 00:39
Total - 172.5073879957199 / 02:53
Step - 38.00087106227875 / 00:38
Total - 171.1903439760208 / 02:51
Step - 36.68382704257965 / 00:37
Total - 170.19031596183777 / 02:50
Step - 35.683799028396606 / 00:36
As you can see, the total time skips from 2:53 to 2:51, but the step timer remains consistent. The reason is the TimeInterval for total goes from 172.5 which gets rounded up, to 171.19 which gets rounded down.
I've also watched the timer count down without touching pause, and it remains in sync reliably. So I've narrowed it down to my pause code.
Fixed my issue and posting here for posterity. I ended up making my totalTimeLeft and currentInterval global properties. Then, on pause and resume, instead of tracking the paused time and adding it to endTime, I just used the totalTimeLeft and currentInterval values that are still stored from the last Timer firing and doing endTime = Date().addingTimeInterval(totalTimeLeft) and the same with the interval time. This got rid of the paused time adding weird amounts that would mess up the rounding.
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.
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 built a basic app with a stopwatch on it. It runs off 3 buttons - play, pause, and reset on a navigation bar which write to a label called timerLabel.
The trouble is as soon as the app is closed or the iPhone goes into sleep mode the stopwatch stops. I've looked for the answer in other questions and it seems that the only way around it is to record the actual time when the app goes off and record the time when it reawakens and compare the difference - then write this to the label
Trouble is I have absolutely no idea how to incorporate that into my current code. I don't even know how to get the time. Here is my code from my view controller.
func updateTimer() {
time++
updateTimeDisplay()
}
func secondsToHoursMinutesSeconds (seconds : Int) -> (Int, Int, Int) {
let h = seconds / 3600
let m = (seconds % 3600) / 60
let s = (seconds % 60)
return (h, m, s)
}
func pauseTimer() {
timer.invalidate()
}
func startTimer() {
if !timer.valid {
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateTimer"), userInfo: nil, repeats: true) }
else {
print("The Stopwatch has already started!")
}
}
func resetTimer() {
time = 0
timer.invalidate()
updateTimeDisplay()
}
func updateTimeDisplay() {
let (h, m, s) = secondsToHoursMinutesSeconds(time)
let timeToDisplay = String(format:"%02d:%02d:%02d", h, m, s)
timerLabel.text = timeToDisplay
}
You cannot use an NSTimer in the background, and your app will not run for long there anyway.
When the application enters background, you should stop the timer and take a note of the current time. Then when the application resumes again you should restart the timer, and calculate how long the app was in the background to update the display accordingly.