This question already has answers here:
update label from background timer
(1 answer)
iOS Swift Timer in not firing if the App is in the background
(2 answers)
Closed 3 years ago.
I'd like to create a simple timer app that works the same way of the native timer of iOS.
To start I just write some simple code that print the second starting to 0 to infinite.
The first problem was that if you go to the home screen, the task obviously stops to work
so I easily sorted just checking the box into background mode - Audio Airplay, and Picture in Picture (inside project - targets - Signing and Capabilities)
now my task works fine.. even in the background.. unless you put your app into a real device
in this case when you go into the background it doesn't work
after that I searched online for a solution and what I've learnt that Apple does't allow the apps to work into the background as you pleased and after 180 seconds the system just "kill" the background task. I just wonder how all the timer app in the Appstore works..
An Interesting thing that I've come across was when I watched an Apple developer conference that they talk about this new framework of background that you basically can make your app working in the background for heavy tasks when the iPhone is charging, and not only that you can forecast when the user will use your app and have some background tasks that work in the background in order to prepare the app to be updated. The link is this https://developer.apple.com/videos/play/wwdc2019/707/
after this I've tried different approaches to sort my problem but nothing has worked yet.. I have followed this tutorial which I found interesting https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9 but it didn't work for me (maybe because of the version of swift outdated or simply because of me) if you guys have managed to make the timer work in the background in your real device let me know.. I would like to understand it well rather than copy and paste the code.
Happy coding to all
the tutorial code:
class RepeatingTimer {
let timeInterval: TimeInterval
init(timeInterval: TimeInterval) {
self.timeInterval = timeInterval
}
private lazy var timer: DispatchSourceTimer = {
let t = DispatchSource.makeTimerSource()
t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
t.setEventHandler(handler: { [weak self] in
self?.eventHandler?()
})
return t
}()
var eventHandler: (() -> Void)?
private enum State {
case suspended
case resumed
}
private var state: State = .suspended
deinit {
timer.setEventHandler {}
timer.cancel()
/*
If the timer is suspended, calling cancel without resuming
triggers a crash. This is documented here https://forums.developer.apple.com/thread/15902
*/
resume()
eventHandler = nil
}
func resume() {
if state == .resumed {
return
}
state = .resumed
timer.resume()
}
func suspend() {
if state == .suspended {
return
}
state = .suspended
timer.suspend()
}
}
Related
THE PROBLEM:
I want to run a simple function, 5 seconds after app goes into background.
I had to implement BGTaskScheduler, to support iOS 13.
The old implementation for BackgroundTask works for me on older iOS versions.
I added background modes as requested (BLE accessories is ticked because we perform a small BLE operation in this function):
Then, I prepared the Info.plist according to the docs (Identifier is fake just for StackOverflow question):
Before didFinishLaunchingWithOptions is ended, I register my BackgroundTask:
if #available(iOS 13.0, *) {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.MyID", using: .global()) { (task) in
print("My backgroundTask is executed NOW!")
task.expirationHandler = {
task.setTaskCompleted(success: true)
}
}
}
Now, when the app run the didEnterBackground method, I submit a BackgroundTaskRequest:
if #available(iOS 13.0, *) {
do {
let request = BGAppRefreshTaskRequest(identifier: "com.example.MyID")
request.earliestBeginDate = Calendar.current.date(byAdding: .second, value: 5, to: Date())
try BGTaskScheduler.shared.submit(request)
print("Submitted task request")
} catch {
print("Failed to submit BGTask")
}
}
The problem here is that it is VERY inconsistent. Apple guarantee that the task will not be executed before the given date, but does not guarantee that it will be executed on the exact time (I'm fine with a small delay).
However, when I ran the app, it did not work 100% of the times, regardless if I provided the task request a delay (using earliestBeginDate) so it used to go first try 7 seconds (instead of 5), next time I submitted the task it took 26 seconds, third time never arrived the closure.
Am I implementing the BackgroundTask the wrong way? I've searched all over the internet some answer but did not encounter anyone having this problem.
As badhanganesh said, It seems like the task will be executed only when the system decides to do so.
Apple said that during WWWDC 2019, session #707.
self.backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
print("animateRightToLeft: went here")
if let indentifier = self.backgroundTaskIdentifier {
print("animateRightToLeft: stop here")
UIApplication.shared.endBackgroundTask(indentifier)
}
})
My App auto killed after some time if App goes background.
Can some one advice is it because of the above code?
It would be much easier to help you if you explain what you are trying to do? The code you provided will only allow your app to execute code in background for limited amount of time (currently 180 seconds on my iPhone 7).
Detailed:
Once you call beginBackgroundTask, you are given a timer which starts running after your app goes to background. While that timer is running, your app will be executing code even in background. When this timer runs out, or you call endBackgroundTask, your code will stop executing in background. Also if that timer runs out before you called endBackgroundTask, your expiration handler will be called and you should call endBackgroundTask there.
Please note that the code you wrote in the expirationHandler will be called only if you don't call endBackgroundTask before timer runs out.
You can use this code to test how it all behaves, e.g. if you run it as is, app will print backgroundTimeRemaining in the console even when in background. If you comment beginBackgroundTask your app will not print anything after it goes to background.
private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier?
var timer: Timer?
#IBAction func buttontapped(_ sender: Any)
{
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block:
{
(timer) in
NSLog("$$$$$ Time remaining: \(UIApplication.shared.backgroundTimeRemaining)")
})
self.backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler:
{
NSLog("$$$$$ Timer expired: Your app will not be executing code in background anymore.")
if let indentifier = self.backgroundTaskIdentifier
{
UIApplication.shared.endBackgroundTask(indentifier)
}
})
NSLog("$$$$$ start")
DispatchQueue.main.asyncAfter(deadline:.now() + 30)
{
NSLog("$$$$$ end")
if let indentifier = self.backgroundTaskIdentifier
{
UIApplication.shared.endBackgroundTask(indentifier)
}
}
}
From Docs beginBackgroundTask(expirationHandler:)
This method requests additional background execution time for your app. Call this method when leaving a task unfinished might be detrimental to your app’s user experience. For example, call this method before writing data to a file to prevent the system from suspending your app while the operation is in progress. Do not use this method simply to keep your app running after it moves to the background.
Each call to this method must be balanced by a matching call to the endBackgroundTask(_:) method.
My App auto killed after some time if App goes background , is it because of the above code?
no it isn't the above snippet only asks for additional time until task is finished , your app will be terminated anyway
OK. Looked through the possible answers, but I don't see my issue here.
I have a fairly bog-standard GCD repeating timer:
class RepeatingGCDTimer {
/// This holds our current run state.
private var state: _State = ._suspended
/// This is the time between fires, in seconds.
let timeInterval: TimeInterval
/// This is the callback event handler we registered.
var eventHandler: (() -> Void)?
/* ############################################################## */
/**
This calculated property will create a new timer that repeats.
It uses the current queue.
*/
private lazy var timer: DispatchSourceTimer = {
let t = DispatchSource.makeTimerSource() // We make a generic, default timer source. No frou-frou.
t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval) // We tell it to repeat at our interval.
t.setEventHandler(handler: { [unowned self] in // This is the callback.
self.eventHandler?() // This just calls the event handler we registered.
})
return t
}()
/// This is used to hold state flags for internal use.
private enum _State {
/// The timer is currently paused.
case _suspended
/// The timer has been resumed, and is firing.
case _resumed
}
/* ############################################################## */
/**
Default constructor
- parameter timeInterval: The time (in seconds) between fires.
*/
init(timeInterval inTimeInterval: TimeInterval) {
self.timeInterval = inTimeInterval
}
/* ############################################################## */
/**
If the timer is not currently running, we resume. If running, nothing happens.
*/
func resume() {
if self.state == ._resumed {
return
}
self.state = ._resumed
self.timer.resume()
}
/* ############################################################## */
/**
If the timer is currently running, we suspend. If not running, nothing happens.
*/
func suspend() {
if self.state == ._suspended {
return
}
self.state = ._suspended
self.timer.suspend()
}
/* ############################################################## */
/**
We have to carefully dismantle this, as we can end up with crashes if we don't clean up properly.
*/
deinit {
self.timer.setEventHandler {}
self.timer.cancel()
self.resume() // You need to call resume after canceling. I guess it lets the queue clean up.
self.eventHandler = nil
}
}
Works great!
...except when it doesn't.
That would be when I put the device into Airplane Mode.
At that point, the timer stops firing.
Even when I come out of Airplane Mode, the timer doesn't restart.
The app uses UIApplication.shared.isIdleTimerDisabled = true/false to keep the app awake, but that doesn't seem to keep the events coming.
Can anyone clue me into what's happening here, and how I might be able to work around it?
This app needs to work in Airplane Mode. In fact, it is most likely to be used in Airplane Mode.
OK. I think I solved this. As is often the case with these things, it's PEBCAK.
I have a routine that stops the timer when the app is backgrounded, and failed to put in a corresponding restart for when it is foregrounded.
When I slide up the Control Center, it backgrounds the app.
My bad.
Yeah, it's embarrassing, but I want to leave this question here as a warning to others.
I have a watchOS 4 app which displays SpriteKit animations (SKActions) on top of the UI. Everything works fine in simulator and also on device first couple of times, then after some time when app is in background, and it is started, animations just freeze and completion block for the most long-lasting animation is not called. Any idea what might be the issue?
This is how I run my actions, caller is waiting for completion closure in order to hide the spritekit scene:
private func runActions(with icon: SKShapeNode?, completion: #escaping () -> Void) {
if let icon = icon, let scaleAction = scaleAction, let bg = background {
self.label?.run(fadeInOutAction)
icon.run(scaleAction)
icon.run(fadeInOutAction)
bg.run(backgroundAction, completion: completion)
} else {
completion()
}
}
And yes, I am aware that SKScene is paused when app moves to background. I am doing this in willActivate of my InterfaceController:
if scene.scene?.isPaused == true {
scene.scene?.isPaused = false
}
I want to emphasize that this works first always. It begins to fail after the app has been backgrounded for some time. Especially if I start the app from complication and try to immediately fire these animations, then this freezing happens.
Can I answer my own question? I guess I can? Here goes:
I finally solved this. It turns out that the WKInterfaceScene in WatchKit has ALSO an isPaused property that you need to turn false sometimes. So now in willActivate of my InterfaceController I will also check that and turn it false if it is true. Since I made this change, I haven't seen a single hiccup, freeze or anything weird anymore.
Case closed, I guess. I leave this here for future generations who might face this issue.
When user launch the app or finish editing the data I need to update local notifications, basically it takes around 2-3 seconds in async way. I need to make sure that this code executes even if app leave foreground. What I have now:
func buildLocalNotifications()
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
//recreate the notifications
}
}
And I can call this method from didFinishLaunchingWithOptions or when user save the form and everything works like a charm while app stays active for more then 3-4 seconds and its not blocking UI of course.. but if user lock the screen or terminate the app - this code won;t finished and notifications won't be created. How to safely execute sensitive code?
What is coming on my mind - show up a loader while performing this action - but it will block the user interaction
Ok I found the solution for the task which requires some time and should not be interrupted when app leaves foreground.
So we need beginBackgroundTask and endBackgroundTask
Small manager which you can use to execute code even when app is not in foreground
class BackgroundTaskManager {
let backgroundDQ = DispatchQueue.global(qos: .background)
var backgroundUpdateTask: UIBackgroundTaskIdentifier!
init(withName: String) {
self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(withName: withName) {}
}
/* Using completion handler to know when code is done*/
func runBackgroundTask(withCode: #escaping (_ cH: #escaping () -> Void) -> Void)
{
backgroundDQ.async {
withCode() {
self.endBackgroungTask()
}
}
}
func endBackgroungTask() {
if backgroundUpdateTask != nil && backgroundUpdateTask != UIBackgroundTaskInvalid {
UIApplication.shared.endBackgroundTask(backgroundUpdateTask)
backgroundUpdateTask = UIBackgroundTaskInvalid
}
}
}
And you can use it like
let taskManager = BackgroundTaskManager(withName: "LocalNotifications")
taskManager.doBackgroundTask() { (cH) in
//Your code goes here
//Send back completion handler so system knows when to finish background task
cH()
}
More information you can find on the Medium
If you want to make sure your code gets executed even if the user closes your app, you need to call your function in applicationWillTerminate. However, you only have ~5 seconds to execute code, before the system closes your app, so asynchronous execution is not encouraged here. It also doesn't matter if you execute code synchronously, since the user already quit your app, so you won't be blocking any UI updates.
Try to excute your code in background
DispatchQueue.global(qos: .background).async {
// your code here
}