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.
Related
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()
}
}
I wanna extend backgroundTimeRemaining more than 30 seconds
and according to Apple
"The value is valid only after the app enters the background and has started at least one task using beginBackgroundTask(expirationHandler:) in the foreground.
System conditions may end background execution earlier, either by calling the expiration handler, or by terminating the app."
so I try to add and edit but it can't work
here's what I tried
//MARK:- BeginBackgroundTask
func registerBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
self?.endBackgroundTask()
print(self!.beginTime)
}
//TODO: Add new background time ex: 60 sec
var backgroundTimeRemaining: TimeInterval {
get{
return 60
}
}
assert(backgroundTask != .invalid)
}
//MARK:- EndBackgroundTask
func endBackgroundTask() {
print("Background task ended.")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
}
backgroundTimeRemaining is informational to your app. The app does not control how much time the system gives it. You can request some background time in order to finish up some user-requested action, and you may receive it, but you don't have any control over how much time. You will need to redesign to not require this.
The point of beginBackgroundTask is to mark finite-length activities that, if the app were to go into the background in the middle, it would be useful to get a few extra seconds to finish up. If, for example, you are starting background tasks in willEnterBackground, or you are not calling a balancing endBackgroundTask in a timely manner, you are probably misusing the system and the system will tend to not give you background time at all.
See Advances in App Background Execution for Apple's latest guidance on background execution.
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
The following tests works fine on iOS 11. It dismisses the alert asking permissions to use the locations services and then zooms in in the map. On iOS 10 or 9, it does none of this and the test still succeeds
func testExample() {
let app = XCUIApplication()
var handled = false
var appeared = false
let token = addUIInterruptionMonitor(withDescription: "Location") { (alert) -> Bool in
appeared = true
let allow = alert.buttons["Allow"]
if allow.exists {
allow.tap()
handled = true
return true
}
return false
}
// Interruption won't happen without some kind of action.
app.tap()
removeUIInterruptionMonitor(token)
XCTAssertTrue(appeared && handled)
}
Does anyone have an idea why and/or a workaround?
Here's a project where you can reproduce the issue: https://github.com/TitouanVanBelle/Map
Update
Xcode 9.3 Beta's Changelogs show the following
XCTest UI interruption monitors now work correctly on devices and simulators running iOS 10. (33278282)
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let allowBtn = springboard.buttons["Allow"]
if allowBtn.waitForExistence(timeout: 10) {
allowBtn.tap()
}
Update .exists to .waitForExistence(timeout: 10), detail please check comments.
I had this problem and River2202's solution worked for me.
Note that this is not a fix to get the UIInterruptionMonitor to work, but a different way of dismissing the alert. You may as well remove the addUIInterruptionMonitor setup. You'll need to have the springboard.buttons["Allow"].exists test anywhere the permission alert could appear. If possible, force it to appear at an early stage of the testing so you don't need to worry about it again later.
Happily the springboard.buttons["Allow"].exists code still works in iOS 11, so you can have a single code path and not have to do one thing for iOS 10 and another for iOS 11.
Incidentally, I logged the base issue (that addUIInterruptionMonitor is not working pre-iOS 11) as a bug with Apple. It has been closed as a duplicate now, so I guess they acknowledge that it is a bug.
I used the #River2202 solution and it works better than the interruption one.
If you decide to use that, I strongly suggest that you use a waiter function. I created this one in order to wait on any kind of XCUIElement to appear:
Try it!
// function to wait for an ui element to appear on screen, with a default wait time of 20 seconds
// XCTWaiter was introduced after Xcode 8.3, which is handling better the timewait, it's not failing the test. It uses an enum which returns: 'Waiters can be used with or without a delegate to respond to events such as completion, timeout, or invalid expectation fulfilment.'
#discardableResult
func uiElementExists(for element: XCUIElement, timeout: TimeInterval = 20) -> Bool {
let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == true"), object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
guard result == .completed else {
return false
}
return true
}
My application needs to have more than 10 local notifications at different time (not recurring) on daily basis. According to iOS official docs, i can only schedule 64 notifications. I have tried solutions from this and several others articles on the web but found no working solution.
Is there any way i can schedule the Local notifications at different times even if my app is not running for several days (or killed)?
There is no direct way For doing this.
If you want to do it anyhow(not proper solution, just a patch), then just go via following way.
wake up the app in background - which can be done by using starting location manager, which will wake up your app in background when location get updated, at that time you can do whatever you like with local notification or any other things.
Before applying this method - make sure that - this is too much battery consuming way + not proper way. Your app might get rejected from apple if it is using too much battery.
Read following details(copied from other question from stackoverflow):
An app can be woken by a significant location change, if the app has indicated that it wants to monitor such events.
See: [CLLocationManager Docs][1]
Look for a method called startMonitoringSignificantLocationChanges. If a significant location change occurs while your app is not in the foreground or isn't running at all, your application will be launched in the background, allowing the app to perform background-only operations (e.g. no view code will run).
When you want your app to work in background even when it is killed then you have to enable 'Background Modes' from your Project's Capabilities and fire your Local Notifications method with specific times through that.
Here is a little code snippet to get you started:
var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
override public func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.reinstateBackgroundTask), name: UIApplicationDidBecomeActiveNotification, object: nil)
}
//MARK: Background Task / Local Notifications / Checkin
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func reinstateBackgroundTask() {
if backgroundTask == UIBackgroundTaskInvalid {
registerBackgroundTask()
}
}
func registerBackgroundTask() {
backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler {
[unowned self] in
self.endBackgroundTask()
}
assert(backgroundTask != UIBackgroundTaskInvalid)
}
func endBackgroundTask() {
NSLog("Background task ended.")
UIApplication.sharedApplication().endBackgroundTask(backgroundTask)
backgroundTask = UIBackgroundTaskInvalid
}
func dosomething() {
registerBackgroundTask()
//Fire Local Notifications accordingly…
//Use NSTimer if you want it with specific time intervals
}
Have a look at this wonderful Raywenderlich Tutorial about Background Modes in iOS.
Apple's Documentation