How to properly measure elapsed time in background in Swift - swift

I have this two functions that measure the elapsed time when the phone is locked or the app is in background:
func saveTimeInBackground(){
startMeasureTime = Int(Date.timeIntervalSinceReferenceDate)
}
func timeOnAppActivated(){
stopMeasureTime = Int(Date.timeIntervalSinceReferenceDate)
elapsedTime = stopMeasureTime - startMeasureTime
seconds = seconds - elapsedTime + 2
if seconds > 0 {
timerLbl.text = "time: \(seconds)"
} else {
seconds = 0
timerLbl.text = "time: \(seconds)"
}
}
and then in the viewDidLoad() i have observers that are trigger the functions when the app becomes active/inactive:
NotificationCenter.default.addObserver(self, selector: #selector(saveTimeInBackground), name: Notification.Name.UIApplicationWillResignActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(timeOnAppActivated), name: Notification.Name.UIApplicationDidBecomeActive, object: nil)
The problem is that when the app becomes active there are 2 seconds (approximately) of difference so i've added 2 seconds and it seems to work fine, but only if the elapsed time is > 15 seconds.
If i lock the phone and immediately unlock it the there are like 5 or more seconds that are missing. For example, if there are 50 seconds left, when i lock and immediately unlock it there are like 42 seconds left.
Can anyone please explain, what i am doing wrong?
Edit: The logic of the app is this:
It starts a match between 2 players with 60 seconds for a game. The problem is that when one of the players locks the phone the app stop to measure the time. This way if the player1 has 10 seconds left to make a move, the player2 still has 50 seconds left. I'm looking for a reliable way to calculate the time even if the player locks the phone or put the app in background.
Edit 2: I think i figured out what the problem is: I think the issue has to do with the fact that the “seconds” are Int, and the Date not and when it gets converted it’s rounded up. I didn't tested it, but when i ahve the solution i'll post the answer. Thanks all for your time!

You're relying on exact timing of notifications that aren't guaranteed to have any exact timing. There's no guarantee about when, exactly, either of those notifications will arrive, and there's nothing you can do about that. Even your two-second fix is, as you say, approximate. It'll probably be different on different models of iPhone or even at different times on the same iPhone, depending how busy iOS is when you check.
What's more, when you go into the background, you can't be certain that you'll stay there. Once in the background, iOS might decide to terminate your app at any time.
I'm not sure what the goal is here but I think you'll need to reconsider what you want to do and see if there's some other approach. Your current two-second hack will, at best, spawn a bunch of other hacks (like the 15 second threshold you mention) without ever being especially accurate. And then it'll probably all break in the next iOS update when some iOS change causes the timing to change.

I would use Date object to track game time.
func gameStart() {
gameStartDate = Date()
}
func timeOnAppActivated() {
let secondsLeft = 60 - abs(gameStartDate?.timeIntervalSinceNow ?? 0)
if secondsLeft > 0 {
timerLbl.text = "time: \(secondsLeft)"
} else {
timerLbl.text = "time: 0"
}
}

Ok, like I mention in the edit 2 of the question:
The first issue was because "seconds" is a Int and then it almost always gains or lose when converting it from Double.
But the main problem was that i had to invalidate the timer when the app enter in background and i didn't.
So now with invalidating the timer when the app gets the notification that will enter background and then starting it when it enter foreground everything works fine.

To test this properly call those methods on button click. It may be coz of delay in releasing some resources in background.

Related

Swift & Xcode IOS App: limit time app is running in background

I would like to know how to limit the time my app is running in background.
The purpose is to show the LaunchScreen again and start the app from initial state after 5 or 10 minutes in background run.
thanks in advance
Here are some ideas and thoughts to achieve what you want.
First, when your app moves into the background, it makes a call to applicationDidEnterBackground(_:) in your App Delegate. If you are using scenes with a Scene Delegate then you should perform any last logic in sceneDidEnterBackground(_:)
As per the docs, you have 5 seconds to perform any task once the above functions have been called
applicationDidEnterBackground(_:)
Return from applicationDidEnterBackground(_:) as quickly as possible.
Your implementation of this method has approximately five seconds to
perform any tasks and return.
After this, your app goes into a suspended state and if you need more than 5 seconds, you can request more by calling beginBackgroundTask(withName:expirationHandler:) but for your purposes I don't think you need this.
More on the above topics here
You have little to no control on when your app gets to run on the background again, that is moved from a suspended to a background state.
There is a property for BGTaskRequest called earliestBeginDate but as per the docs:
earliestBeginDate
Specify nil for no start delay.
Setting the property indicates that the background task shouldn’t
start any earlier than this date. However, the system doesn’t
guarantee launching the task at the specified date, but only that it
won’t begin sooner.
So long story short, you can't really tell when your app will get to run on the background again and you can't decide how much time it will be given to run.
What you could do is implement either applicationDidEnterBackground(_:) or sceneDidEnterBackground(_:) if you are using scenes to track at what time the app goes into the background like so:
func sceneDidEnterBackground(_ scene: UIScene)
{
// Get the current date / time
let currentDate = Date()
let userDefaults = UserDefaults.standard
// Start tracking time as soon as the app goes
// into the background
userDefaults.setValue(currentDate,
forKey: "PreviousLaunchDate")
// Just for testing, remove from production app
print("start tracking")
}
And then when your app comes into the the foreground, check how much time has elapsed and react accordingly:
func sceneWillEnterForeground(_ scene: UIScene)
{
let userDefaults = UserDefaults.standard
let currentDate = Date()
// Check if you have saved a date before
if let previousLaunchDate
= userDefaults.object(forKey: "PreviousLaunchDate") as? Date
{
// Retrieve the minutes elapsed
// Check if minutes elapsed is greater than 10 minutes or as you wish
if let minutesElapsed = Calendar.current.dateComponents([.minute],
from: previousLaunchDate,
to: currentDate).minute,
minutesElapsed > 10
{
// Reset your window's root view controller to start again
// showing the splash view or reset your storyboard etc
// For example
// let splashVC = SplashViewController()
// window?.rootViewController = splashVC
// just for testing, remove from production app
print("show splash vc")
return
}
// Do nothing since 10 minutes haven't elapsed so you can
// show the current state of the app to the user
// just for testing, remove from production app
print("do nothing")
}
}
I think this should get you close to what you are looking for.

How to detect (say) 2 seconds of inactivity in swift?

This question could be rephrased as: How to invoke a function if 2 seconds pass without an event (re)occurring?
I'm playing with SFSpeechRecogniser. While the user is speaking it sends continuous updates (maybe 2-3 per second). I'm trying to detect when the user stops speaking. If I don't receive any updates for (say) 2 seconds, I can assume that the user has paused speaking.
How to implement this in Swift?
I am aware that I could do:
var timer : Timer?
func f() {
on_event = { result, error in
print( "Got event, restarting timer" )
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in
print( "2s inactivity detected" )
self.timer?.invalidate()
NotificationCenter.default.post( name: inactivity_notification, object: nil )
}
}
}
But is it possible to do it without repeatedly creating and destroying the Timer instance (and thus creating a boatload of temporary Timer instances that never get used)?
One way to do it is to:
Record the current time when an event occurs
Set up a recurring timer with a granularity you are comfortable with (for example 0.25 seconds).
When the timer pops, check difference between current time and last event time. If that is greater than 2 seconds, fire your notification.
This is what I'd do if I had to recognize that a person had stopped typing for 2 seconds. Invalidating and creating timers at typing speed would be a lot of churn. You can tune this to your requirements depending on how close to exactly 2 seconds you need to be.
You could also do this by just having a timeSinceLastEvent variable, and set it to 0 when an event occurs. The recurring timer would increment this by the granularity, and check if it has reached 2 seconds and fire the notification if it had. This is cruder than doing the time math since the timer interval isn't guaranteed, but simpler.
Timer's .fireDate property is writable.
So every time a speech event occurs just do timer.fireDate = Date(timeIntervalSinceNow: 2)

Alternative for Timers

I am having a simple game with a player, a moving background and moving walls, kinda like flappy bird. My player is able to collect a powerUp. It is transforming after and put into a special level.
I want this level to run for like 10 seconds and then call a backtransformation.
Currently I am using a timer which just calls the backtransformation after 10 seconds. I am having trouble now when the player pauses the game, the timer is still running.
--> what I found on stackoverflow is that you can't resume a timer, you can just invalidate and restart it. This would miss my target, otherwise the player pauses the game every 9 seconds and can stay in the super Level forever.
Do you guys have any idea how I can solve my problem or like an alternative to use Timers in my code?
I appreciate any help
Edit: Here is how I simply used the timer
// transform and go into super level added here
self.transform()
timer = Timer.scheduledTimer(withTimeInterval:10, repeats: false) {
timer in
self.backtransform()
}
When you start the timer record the current time as a Double using
let start = Date().timeIntervalSinceReferenceDate
var remaining = 10.0
When the user pauses the timer, calculate the amount of time that has passed with:
let elapsed = Date().timeIntervalSinceReferenceDate - start
And the amount of remaining time for your timer:
remaining -= elapsed
If the user resumes the timer, set it to remaining, not 10 seconds.

Schedule function execution in swift

I'm developing a simple app in Swift and I need to schedule a function execution every 24 hours. I'm aware of the method:
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0, execute: {
self.functionToCall()
})
that could solve my problem but, is this the right solution for a 24 hours delay?
Thanks
Theoretically, this is possible.
The problem is that your app would have to run in the foreground for 24 hours, which is very unlikely to happen. Unfortunately, you can not run background tasks just like that.
The solution:
Just make it look like the function would execute in the background. Every time the update function is called, simply save the Int(Date().timeIntervalSince1970) to UserDefaults. This works like a timestamp and saves the last time you called your update function. Every time in the viewDidLoad()-function (not sure if it's called the same on Mac apps, but you can imagine what I mean) call:
If let timestamp = UserDefaults.standard.integer(forKey: "yourTimestampKey") {
let currentTimestamp = Date().timeIntervalSince1970
if (currentTimestamp - timestamp) > 86400 { // number of seconds in 24 hours
// the last time your function was updated was at least 24h ago
update()
}
}
That's how you can make it appear like it was updated in the background. I use this all the time in my apps and it works perfectly.
EDIT:
Maybe, just in case the app does indeed run 24 hours in a row, I would set up the upper function that you posted first as well.

Synchronize the playback of two or more AVAudioPlayer in Iphone

I need to play 2 sounds with 2 AVAudioPlayer objects at the same exact time... so I found this example on Apple AVAudioPlayer Class Reference (https://developer.apple.com/library/mac/#documentation/AVFoundation/Reference/AVAudioPlayerClassReference/Reference/Reference.html):
- (void) startSynchronizedPlayback {
NSTimeInterval shortStartDelay = 0.01; // seconds
NSTimeInterval now = player.deviceCurrentTime;
[player playAtTime: now + shortStartDelay];
[secondPlayer playAtTime: now + shortStartDelay];
// Here, update state and user interface for each player, as appropriate
}
What I don't understand is: why also the secondPlayer has the shorStartDelay?
Shouldn't it be without? I thought the first Player needed a 0.1 sec delay as it is called before the second Player... but in this code the 2 players have the delay...
Anyone can explain me if that is right and why?
Thanks a lot
Massy
If you only use the play method ([firstPlayer play];), firstPlayer will start before the second one as it will receive the call before.
If you set no delay ([firstPlayer playAtTime:now];), the firstPlayer will also start before de second one because firstPlayer will check the time at which it is supposed to start, and will see that it's already passed. Thus, it will have the same behaviour as when you use only the play method.
The delay is here to ensure that the two players start at the same time. It is supposed to be long enough to ensure that the two players receive the call before the 'now+delay' time has passed.
I don't know if I'm clear (English is not my native langage). I can try to be more clear if you have questions
Yeah what he said ^ The play at time will schedule both players to start at that time (sometime in the future).
To make it obvious, you can set "shortStartDelay" to 2 seconds and you will see there will be a two second pause before both items start playing.
Another tip to keep in mind here are that when you play/pause AVAudioPlayer they dont actually STOP at exactly the same time. So when you want to resume, you should also sync the audio tracks.
Swift example:
let currentDeviceTime = firstPlayer.deviceCurrentTime
let trackTime = firstPlayer.currentTime
players.forEach {
$0.currentTime = trackTime
$0.play(atTime: currentDeviceTime + 0.1)
}
Where players is a list of AVAudioPlayers and firstPlayer is the first item in the array.
Notice how I am also resetting the "currentTime" which is how many seconds into the audio track you want to keep playing. Otherwise every time the user plays/pauses the track they drift out of sync!