Schedule function execution in swift - 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.

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 properly measure elapsed time in background in 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.

Strange date picker behaviour Xcode, swift 3

My current project is a timer which uses a date picker to set the amount of time the user wants before the timer goes off (say 1 minute, 6 hours and two minutes etc.). The problem lies in the amount of time that the date picker believes it has been set for. Below is the code which I am using to set the time.
#IBOutlet weak var datePicker: UIDatePicker!
var timeAmount:Double = 0
#IBAction func startButton() {
timeAmount = Double(datePicker.countDownDuration)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeAmount, repeats: false)
}
Here it can be seen that the function startButton, sets the value of timeAmount to be the amount of time that the date picker is set for. This timeAmount value is then used in a local notification as the time.
The issue is that the datePicker.countDownDuration is never correct. If it is set for one minute, then timeAmount may return a value of 99 seconds, 62 seconds, 67 seconds etc. It is just completely random.
Maybe I do not entirely understand how the .countDownDuration feature works, however from everything I have read, this should return a value of 60 seconds.
Any thoughts and suggestions on the matter will be very much appreciated, thank you.
By default, the UIDatePicker will take the current second value for the calculation.
timeAmount = (number of minutes selected in picker * 60) + current time second value.
For example:
If the current time is 13:40:25 (1 PM 40 minutes 25 seconds) and you have selected one minute in the date picker, the value of timeAmount in this case is 85.
timeAmount = (1*60) + 25
This will solve your problem.
Go to your Storyboard and select UIDatapicker. Navigate to Date option in Attributes inspector.
Click the Dropdown and change it to Custom from Current Date.
You will see a new text field with the current time as the custom date.
Just change the second field to 00.
Run the App now. Now it will take 0 as the value for the second and you will able to see seconds value correctly based on the time you are choosing in the date picker.
Hope this will solve your problem.

Play Framework - Schedule a task at precise time [duplicate]

This question already has answers here:
scala - How to run a task every day a 11PM
(2 answers)
Closed 6 years ago.
I'm doing a Scala - Play application and I want to schedule a task to sending mail everyday at 3 A.M, so I create a class for that but i'ts not working:
class ManageSendInvalidateDaemon #Inject() (app: Application, emailSender: EmailSender, mailer: MailerClient) {
Akka.system(app).scheduler.schedule(
scala.concurrent.duration.Duration.create(20,TimeUnit.SECONDS),
scala.concurrent.duration.Duration.create(60, TimeUnit.SECONDS),
new Runnable(){
override def run()={
//Function to send the mail
}
}
);
};
I think the problem is here:
scala.concurrent.duration.Duration.create(20,TimeUnit.SECONDS),
scala.concurrent.duration.Duration.create(60, TimeUnit.SECONDS),
I don't really understand what it's the use of these 2 lines
That will not work as you expect. Akka scheduler just lets you specify the recurrency of the task, but not the day, hour etc when it will run (ex: you can tell it to run a task every 10 minutes, but not run this at 15:30 each Monday).
Those two lines instruct Akka to run that task every 60 seconds, and run the first time 20 seconds after you define it (so if the schedule call is executed at 12h30m that task will run the first time at 12:30:20 and then 12:31:20, 12:32:20, etc).
To overcome this, you just need to run a task periodically (in your case, each minute for example), and check the current hour. If it is 3AM send those emails (and eventually store anywere that this task was executed).
Another option is to use something like akka-quartz-scheduler, that lets you specify that type of scheduling
I had the same problem. So I created a method to calculate how much time from now until the time I want to run my
schedule = Akka.system().scheduler().schedule(
Duration.create(nextRunTime(), TimeUnit.MILLISECONDS),
Duration.create(DAY_IN_MS, TimeUnit.MILLISECONDS),
...,);
The nextRunTime() method calculates how much time in milliseconds will be until the next 3 AM and after that I set an interval of one day (DAY_IN_MS)
private int nextRunTime() {
String runTimeValue = "03:00";
LocalTime now = new LocalTime();
LocalTime runTime = new LocalTime(runTimeValue);
int timeUntilRun;
if (runTime.isAfter(now)) { // From now until the run time
timeUntilRun = runTime.getMillisOfDay() - now.getMillisOfDay();
} else { // From now until midnight, plus from midnight until the run time
timeUntilRun = DAY_IN_MS - now.getMillisOfDay() + runTime.getMillisOfDay();
}
Logger.info("nextRunTime(): next run in " + timeUntilRun + " ms");
return timeUntilRun;
}

swift - Delay a spawn function using NSTimer

I'm looking to integrate some delayed functions into my game, currently I'm working on putting a 5 second delay on calling my first function as in this time period the game goes through a countdown to get the player ready.
Currently my countdown is displayed but the game is in full swing in the background. I've got one function I'd like to delay for 5 seconds but I'm also thinking of using the same method to incorporate other objects later in the game to boost difficulty.
The call I'm looking to delay is moving.addChild(crows)
If anyone can show me how to build that would be great as I've been unable to find on site (unless I've missed it).
Don't use NSTimer in SpriteKit. Use this instead:
let delay = SKAction.waitForDuration(5.0)
someNode.runAction(delay) {
//run code here after 5 secs
}
let delay = 5.0 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue()) {
moving.addChild(crows)
}