How to prevent Timer slowing down in background - swift

I am writing a Mac OS app in Swift and want to repeat a task every 0.5s (more or less, high precision is not required). This app should run in the background while I use other applications.
I'm currently using a Timer:
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true)
It starts fine with updates roughly every 0.5s but after some time in the background, the Timer slows down considerably to intervals of roughly 1s or 2s (it's very close to these values to it seems that the timer skips ticks or has a slowdown of a factor 2 or 4).
I suspect it's because the app is given a low priority after a few seconds in the background. Is there a way to avoid this? It can be either in the app settings in XCode by asking to stay active all the time, or possibly from the system when the app is run (or even but doing things differently without Timer but I'd rather keep it simple if possible).
Here is a minimal working example: the app only has a ViewController with this code
import Cocoa
class ViewController: NSViewController {
var lastUpdate = Date().timeIntervalSince1970
override func viewDidLoad() {
super.viewDidLoad()
let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) {
timer in
let now = Date().timeIntervalSince1970
print(now - self.lastUpdate)
self.lastUpdate = now
}
RunLoop.current.add(timer, forMode: .common)
}
}
Output at start is
0.5277011394500732
0.5008649826049805
0.5000109672546387
0.49898695945739746
0.5005381107330322
0.5005340576171875
0.5000457763671875
...
But after a few seconds in the background it becomes
0.49993896484375
0.49997520446777344
0.5000619888305664
1.5194149017333984
1.0009620189666748
0.9984869956970215
2.0002501010894775
2.001321792602539
1.9989290237426758
...
If I bring the app back to the foreground, the timer goes back to 0.5s increments.
Note: I'm running Mac OSX 10.15.5 (Catalina) on an iMac

This is because of the App Nap. You can disable App Nap but it is not recommended.
var activity: NSObjectProtocol?
activity = ProcessInfo().beginActivity(options: .userInitiatedAllowingIdleSystemSleep, reason: "Timer delay")
The default tolerance value of timer is zero but The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of tolerance property.

As I stated in my comment below, if you want granularities lower than 1.0 s, you should not use Timer objects, but rather GCD. I wrote a class MilliTimer you can use where you have improved granularity down to a few milliseconds. Please try this in a Playground and then in your app. In this example, I set the granularity of the timer based on GCD to 50 milliseconds. To adjust the delay pass the delay you want in milliseconds in the respective parameter of the initializer. In your case, you might be interested in 500 ms = 0.5 s.
import Cocoa
public class MilliTimer
{
static let µseconds = 1000000.0
static var lastUpdate = DispatchTime.now()
var delay = 0
var doStop = false
var runs = 0
let maxRuns = 50
private class func timer(_ milliseconds:Int, closure:#escaping ()->())
{
let when = DispatchTime.now() + DispatchTimeInterval.milliseconds(milliseconds)
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
init(delay:Int) {
self.delay = delay
}
func delta() -> Double {
let now = DispatchTime.now()
let nowInMilliseconds = Double(now.uptimeNanoseconds) / MilliTimer.µseconds
let lastUpdateInMilliseconds = Double(MilliTimer.lastUpdate.uptimeNanoseconds) / MilliTimer.µseconds
let delta = nowInMilliseconds - lastUpdateInMilliseconds
MilliTimer.lastUpdate = now
return delta
}
func scheduleTimer()
{
MilliTimer.timer(delay) {
print(self.delta())
if self.doStop == false {
self.scheduleTimer()
self.runs += 1
if self.runs > self.maxRuns {
self.stop()
}
}
}
}
func stop() {
doStop = true
}
}
MilliTimer(delay: 50).scheduleTimer()
CFRunLoopRun()

Related

Step Counter Sprite Kit

I am trying to implement a step counter in my sprite Kit game.
And it should work like this:
The counter adds 1 to a value each second.
Every fifth second the duration (in this case 1) gets divided by 1.1
But if I create a func that returns the new duration, the repeat forever SKAction only uses this value for one time and then the duration never changes again.
you should make an action that calls itself, rather than using SKAction.repeatForever(...). you can recalculate values that way. not sure i entirely understand your use case, but here is an example that fires an event after a duration, and modifies that duration every fifth cycle.
var isLoopEnabled:Bool = true
var counter:Int = 0
var duration:TimeInterval = 1.0
func updateDuration() {
duration /= 1.1
}
/*
creates an event loop. the action waits, fires, then calls itself again (before exiting)
turn the loop off using the isLoopEnabled flag
*/
func loop() {
let wait = SKAction.wait(forDuration: duration)
let run = SKAction.run {
self.counter += 1 //increment counter
//update duration every fifth count
if self.counter % 5 == 0 {
self.updateDuration()
}
}
let end = SKAction.run{
print("\(self.counter) -- duration: \(self.duration)")
guard self.isLoopEnabled else { return } //flag allows you to exit loop
self.loop() //repeats by calling itself
}
let sequence = SKAction.sequence([ wait, run, end ])
self.run(sequence, withKey:"loop action")
}
override func didMove(to view: SKView) {
loop()
}

DispatchSourceTimer schedule (GCD swift macOS) stops after 40 seconds when the app is hidden [duplicate]

I am writing a Mac OS app in Swift and want to repeat a task every 0.5s (more or less, high precision is not required). This app should run in the background while I use other applications.
I'm currently using a Timer:
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true)
It starts fine with updates roughly every 0.5s but after some time in the background, the Timer slows down considerably to intervals of roughly 1s or 2s (it's very close to these values to it seems that the timer skips ticks or has a slowdown of a factor 2 or 4).
I suspect it's because the app is given a low priority after a few seconds in the background. Is there a way to avoid this? It can be either in the app settings in XCode by asking to stay active all the time, or possibly from the system when the app is run (or even but doing things differently without Timer but I'd rather keep it simple if possible).
Here is a minimal working example: the app only has a ViewController with this code
import Cocoa
class ViewController: NSViewController {
var lastUpdate = Date().timeIntervalSince1970
override func viewDidLoad() {
super.viewDidLoad()
let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) {
timer in
let now = Date().timeIntervalSince1970
print(now - self.lastUpdate)
self.lastUpdate = now
}
RunLoop.current.add(timer, forMode: .common)
}
}
Output at start is
0.5277011394500732
0.5008649826049805
0.5000109672546387
0.49898695945739746
0.5005381107330322
0.5005340576171875
0.5000457763671875
...
But after a few seconds in the background it becomes
0.49993896484375
0.49997520446777344
0.5000619888305664
1.5194149017333984
1.0009620189666748
0.9984869956970215
2.0002501010894775
2.001321792602539
1.9989290237426758
...
If I bring the app back to the foreground, the timer goes back to 0.5s increments.
Note: I'm running Mac OSX 10.15.5 (Catalina) on an iMac
This is because of the App Nap. You can disable App Nap but it is not recommended.
var activity: NSObjectProtocol?
activity = ProcessInfo().beginActivity(options: .userInitiatedAllowingIdleSystemSleep, reason: "Timer delay")
The default tolerance value of timer is zero but The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of tolerance property.
As I stated in my comment below, if you want granularities lower than 1.0 s, you should not use Timer objects, but rather GCD. I wrote a class MilliTimer you can use where you have improved granularity down to a few milliseconds. Please try this in a Playground and then in your app. In this example, I set the granularity of the timer based on GCD to 50 milliseconds. To adjust the delay pass the delay you want in milliseconds in the respective parameter of the initializer. In your case, you might be interested in 500 ms = 0.5 s.
import Cocoa
public class MilliTimer
{
static let µseconds = 1000000.0
static var lastUpdate = DispatchTime.now()
var delay = 0
var doStop = false
var runs = 0
let maxRuns = 50
private class func timer(_ milliseconds:Int, closure:#escaping ()->())
{
let when = DispatchTime.now() + DispatchTimeInterval.milliseconds(milliseconds)
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
init(delay:Int) {
self.delay = delay
}
func delta() -> Double {
let now = DispatchTime.now()
let nowInMilliseconds = Double(now.uptimeNanoseconds) / MilliTimer.µseconds
let lastUpdateInMilliseconds = Double(MilliTimer.lastUpdate.uptimeNanoseconds) / MilliTimer.µseconds
let delta = nowInMilliseconds - lastUpdateInMilliseconds
MilliTimer.lastUpdate = now
return delta
}
func scheduleTimer()
{
MilliTimer.timer(delay) {
print(self.delta())
if self.doStop == false {
self.scheduleTimer()
self.runs += 1
if self.runs > self.maxRuns {
self.stop()
}
}
}
}
func stop() {
doStop = true
}
}
MilliTimer(delay: 50).scheduleTimer()
CFRunLoopRun()

Testing a Timer in Xcode with XCTest

I have a function that does not need to be called any more than every 10 secs. Every time I invoke the function, I reset the timer to 10 secs.
class MyClass {
var timer:Timer?
func resetTimer() {
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) {
(timer) -> Void in
self.performAction()
}
}
func performAction() {
// perform action, then
self.resetTimer()
}
}
I would like to test that calling performAction() manually resets the timer to 10 secs, but I can't seem to find any good way to do it. Stubbing resetTimer() feels like the test wouldn't really be telling me enough about the functionality. Am I missing something?
XCTest:
func testTimerResets() {
let myObject = MyClass()
myObject.resetTimer()
myObject.performAction()
// Test that my timer has been reset.
}
Thanks!
If you want to wait for the timer to fire, you'll still need to use expectations (or Xcode 9's new asynchronous testing API).
The question is what precisely you're trying to test. You presumably don't want to just test that the timer fired, but rather you want to test what the timer's handler is actually doing. (Presumably you have a timer in order to perform something meaningful, so that's what we should be testing.)
WWDC 2017 video Engineering for Testability offers a nice framework to be thinking about how to design code for unit tests , which need:
control over inputs;
visibility to outputs; and
no hidden state.
So, what are the inputs to your test? And, more importantly, what is the output. What assertions do you want to test for in your unit test?
The video also shows a few practical examples of how one might refactor code to achieve this structure through judicious use of:
protocols and parameterization; and
separating logic and effects.
It's hard to advise further without knowing what the timer is actually doing. Perhaps you can edit your question and clarify.
Good that you found a solution, but answering the question in title;
To test if timer actually works (i.e. runs and calls callback), we can do something like:
import XCTest
#testable import MyApp
class MyClassTest: XCTestCase {
func testStartTimer_shouldTriggerCallbackOnTime() throws {
let exp = expectation(description: "Wait for timer to complete")
// Dummy.
let instance: MyClass! = MyClass()
instance.delay = 2000; // Mili-sec equal 2 seconds.
instance.callback = { _ in
exp.fulfill();
}
// Actual test.
instance.startTimer();
// With pause till completed (sleeps 5 seconds maximum,
// else resumes as soon as "exp.fulfill()" is called).
if XCTWaiter.wait(for: [exp], timeout: 5.0) != .completed {
XCTFail("Timer didn't finish in time.")
}
}
}
When having a class like:
public class MyClass {
public var delay: Int = 0;
public var callback: ((timer: Timer) -> Void)?
public func startTimer() {
let myTimer = Timer(timeInterval: Double(self.delay) / 1000.0, repeats: false) {
[weak self] timer in
guard let that = self else {
return
}
that.callback?(timer)
}
RunLoop.main.add(myTimer, forMode: .common)
}
}
First, I would say I don't know how your object was working when you don't any member called refreshTimer.
class MyClass {
private var timer:Timer?
public var starting:Int = -1 // to keep track of starting time of execution
public var ending:Int = -1 // to keep track of ending time
init() {}
func invoke() {
// timer would be executed every 10s
timer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(performAction), userInfo: nil, repeats: true)
starting = getSeconds()
print("time init:: \(starting) second")
}
#objc func performAction() {
print("performing action ... ")
/*
say that the starting time was 55s, after 10s, we would get 05 seconds, which is correct. However for testing purpose if we get a number from 1 to 9 we'll add 60s. This analogy works because ending depends on starting time
*/
ending = (1...9).contains(getSeconds()) ? getSeconds() + 60 : getSeconds()
print("time end:: \(ending) seconds")
resetTimer()
}
private func resetTimer() {
print("timer is been reseted")
timer?.invalidate()
invoke()
}
private func getSeconds()-> Int {
let seconds = Calendar.current.component(.second, from: Date())
return seconds
}
public func fullStop() {
print("Full Stop here")
timer?.invalidate()
}
}
Testing (explanation in the comments)
let testObj = MyClass()
// at init both starting && ending should be -1
XCTAssertEqual(testObj.starting, -1)
XCTAssertEqual(testObj.ending, -1)
testObj.invoke()
// after invoking, the first member to be changed is starting
let startTime = testObj.starting
XCTAssertNotEqual(startTime, -1)
/*
- at first run, ending is still -1
- let's for wait 10 seconds
- you should use async method, XCTWaiter and expectation here
- this is just to give you a perspective or way of structuring your solution
*/
DispatchQueue.main.asyncAfter(deadline: .now() + 10 ) {
let startTimeCopy = startTime
let endingTime = testObj.ending
XCTAssertNotEqual(endingTime, -1)
// take the difference between start and end
let diff = endingTime - startTime
print("diff \(diff)")
// no matter the time, diff should be 10
XCTAssertEqual(diff, 10)
testObj.fullStop()
}
this is not the best of way of doing it, however it gives you view or a flow on how you should achieve this :)
I ended up storing the original Timer's fireDate, then checking to see that after the action was performed the new fireDate was set to something later than the original fireDate.
func testTimerResets() {
let myObject = MyClass()
myObject.resetTimer()
let oldFireDate = myObject.timer!.fireDate
myObject.performAction()
// If timer did not reset, these will be equal
XCTAssertGreaterThan(myObject.timer!.fireDate, oldFireDate)
}

swift spritekit increase frequency of node creation as time goes on

I have figured out how to continuously spawn a node every x seconds. However, I would like to decrease the time that I wait to create a node as the game goes on, to increase the difficulty. For example, I call this function in didMoveToView:
func createSequentialEnemies(){
runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(createEnemy),
SKAction.waitForDuration(2.0)
])
))
}
This creates an enemy every 2 seconds, but I want to decrease this duration arbitrarily. For example, say that after 30 seconds of gameplay I want to now spawn enemies every 1.5 seconds. How would I change the duration dynamically?
Create a spawnDuration property and key reference in your scene class.
class SomeClass: SKScene {
private var spawnDuration: NSTimeInterval = 2.0
private let spawnKey = "SpawnKey"
}
Than adjust your spawn code to use this spawn property and key. I slightly changed the syntax as well to make it more legible in my opinion.
func createSequentialEnemies(){
removeActionForKey(spawnKey) // remove previous action if running. This way you can adjust the spawn duration property and call this method again and it will cancel previous action.
let spawnAction = SKAction.runBlock(createEnemy)
let spawnDelay = SKAction.waitForDuration(spawnDuration)
let spawnSequence = SKAction.sequence([spawnAction, spawnDelay])
runAction(SKAction.repeatActionForever(spawnSequence), withKey: spawnKey) // run action with key so you can cancel it later
}
Than you have to add some logic of when you want to change the spawn duration property you created.
Time based could be a func like this you also call once in DidMoveToView
func startDifficultyTimer() {
let difficultyTimerKey = "DifficultyTimerKey"
let action1 = SKAction.waitForDuration(30)
let action2 = SKAction.runBlock { [unowned self] in
guard self.spawnDuration > 0.2 else { // set a min limit
removeActionForKey(difficultyTimerKey) // if min duration has been reached than you might as well stop running this timer.
return
}
self.spawnDuration -= 0.5 // reduce by half a second
self.createSequentialEnemies() // spawn enemies again
}
let sequence = SKAction.sequence([action1, action2])
runAction(SKAction.repeatActionForever(sequence), withKey: difficultyTimerKey)
}
Hope this helps

Swift - SpriteKit - Update SKAction.waitForDuration while running forever

I am trying to update the waitForDuration action in a sequence that is running forever.
override func didMoveToView(view: SKView) {
//code
runAction(SKAction.repeatActionForever(runSeq()))
}
func runSeq() -> SKAction{
var difficulty: CGFloat = 0.75
let updateAction = SKAction.runBlock({
self.runCount++
self.runCount %= 4
println(self.runCount)
if self.runCount == 0 {
difficulty -= 0.1
}
if self.children.count > 51{
println("You loose")
}else{
self.scoreLabel.text = String(format: "Score: %i", self.score)
}
})
let createAntAction = SKAction.runBlock({self.createAnt()})
var wait = SKAction.waitForDuration(NSTimeInterval(difficulty))
let seq = SKAction.sequence([createAntAction, wait, updateAction])
return seq
}
difficulty is the value that I want, and every 4 loops I want it to decrease by a certain amount. Currently, the starting value remains the same, and doesn't change, even though I am changing the difficulty value, and recalling the sequence of actions every time.
This is happening because wait is already established as .75, and nothing is changing that. SKAction.waitForDuration(NSTimeInterval(difficulty)) is not looking for a reference to difficulty, only a value, so you need to rethink how you want to code this.