Making NSTimer run in the background on a physical device - ios

I have a timer app that I have made.
When I run it on the iPhone X on the simulator, the timer will run in the background just fine, but when I test it on my physical device (iPad Pro), it does not run in the background.
When I say in the background, I mean when you press the off button located on the top (iPad)/side (iPhone) of the device, so that the screen goes black.
Is this a glitch on the simulator's behalf, or something that I am not aware of, or is there something I need to change about my app to make it work in the background on PHYSICAL DEVICES as well?
Thanks

In DidBackground:
stop normalTimer.
start New backgroundTaskTimer
In WillForeground:
stop backgroundTaskTimer.
start normalTimer
Find below sample code:
In Appdelegate:
Declaration:
var backgroundUpdateTask: UIBackgroundTaskIdentifier!
var backgroundTaskTimer:Timer! = Timer()
Implementation:
func doBackgroundTask() {
DispatchQueue.global(qos: .default).async {
self.beginBackgroundTask()
if self.backgroundTaskTimer != nil {
self.backgroundTaskTimer.invalidate()
self.backgroundTaskTimer = nil
}
//Making the app to run in background forever by calling the API
self.backgroundTaskTimer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(self.startTracking), userInfo: nil, repeats: true)
RunLoop.current.add(self.backgroundTaskTimer, forMode: RunLoopMode.defaultRunLoopMode)
RunLoop.current.run()
// End the background task.
self.endBackgroundTask()
}
}
func beginBackgroundTask() {
self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(withName: "Track trip", expirationHandler: {
self.endBackgroundTask()
})
}
func endBackgroundTask() {
UIApplication.shared.endBackgroundTask(self.backgroundUpdateTask)
self.backgroundUpdateTask = UIBackgroundTaskInvalid
}
In ApplicationDidEnterBackground:
func applicationDidEnterBackground(_ application: UIApplication) {
//Start the background fetch
self.doBackgroundTask()
}
In ApplicationWillEnterForeground:
func applicationWillEnterForeground(_ application: UIApplication) {
if self.backgroundTaskTimer != nil {
self.backgroundTaskTimer.invalidate()
self.backgroundTaskTimer = nil
}
}

Related

Timers not running when screen is turned off / device is locked in iOS

The app is in the background and it receives a callback upon disconnection with the BLE device, after which the app has to wait for sometime(1minute) and then execute some piece of code. The app behaves as expected even when in the background if the screen is turned on. But if the screen is turned off then the timer is not working and the app is not executing as expected.
This is the code in AppDelegate to start a timer in background:
func startTimerWith(timeInterval: TimeInterval) {
registerBackgroundTask()
timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: false, block: { (timer) in
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "Time"), object: nil)
self.endBackgroundTask()
})
}
func registerBackgroundTask() {
backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {
self.endBackgroundTask()
})
}
func endBackgroundTask() {
print("Background task ended.")
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = UIBackgroundTaskInvalid
timer?.invalidate()
timer = nil
}
When disconnected from the BLE device, I start the timer by registering to background task:
func disconnected(_ peripheral: CBPeripheral, with error: Error?) {
print("DISCONNECTED!!!")
AppDelegate.sharedApp().startTimerWith(timeInterval: TimeInterval(TIME))
BLEDeviceHandler.sharedInstance.handleBLEDevice(connectedPeripheral!)
}
Two points are vital here :
Timer doesn't work if the app is in background state for more than 10 minutes. I had an exact scenario where I had to perform some
action in background. I found out that after 10 minutes, timer didn't
work.
Timers do not work when the device is locked. App gets suspended immediately once the device is locked. This is for iOS >= 7.0
Bug is fixed its because app using location services but I forgot to given permission to update location when app is in background.

iOS How to detect call state in background?

I'm trying to change button state depending on call state.
I used code from here to detect call state: How to get a call event using CTCallCenter:setCallEventHandler: that occurred while the app was suspended?
And it works fine when app is in foreground. But it doesn't work in background at all. In documentation for CTCallCenter.callEventHandler:
When your application resumes the active state, it receives a single
call event for each call that changed state—no matter how many state
changes the call experienced while your application was suspended. The
single call event sent to your handler, upon your application
returning to the active state, describes the call’s state at that
time.
But I don't get any call events when app resumes active. All I get is last saved call state when app was in foreground. How can I detect call state in background?
Here is my code:
AppDelegate.swift
let callСenter = CTCallCenter()
func block (call:CTCall!)
{
callState = String(call.callState)
print(call.callState)
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
//check for call state
callСenter.callEventHandler = block
...
return true
}
ViewController.swift
override func viewDidLoad()
{
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(cameBackFromSleep),
name: NSNotification.Name.UIApplicationDidBecomeActive,
object: nil
)
...
}
func cameBackFromSleep()
{
self.viewWillAppear(true)
}
override func viewWillAppear(_ animated: Bool)
{
switch callState
{
case "CTCallStateConnected":
print("callState: ", callState)
self.textLabel.isHidden = true
startBtnAnimation()
case "CTCallStateDisconnected":
print("callState: ", callState)
self.textLabel.center.y += self.view.bounds.height
self.textLabel.isHidden = false
stopBtnAnimation()
default: break
}
}
Finally, I solved it! I used code from this answer: Find if user is in a call or not?
I removed everything from AppDelegate, all job is done in ViewController:
override func viewDidLoad()
{
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(cameBackFromSleep),
name: NSNotification.Name.UIApplicationDidBecomeActive,
object: nil
)
...
}
private func isOnPhoneCall() -> Bool
{
let callCntr = CTCallCenter()
if let calls = callCntr.currentCalls
{
for call in calls
{
if call.callState == CTCallStateConnected || call.callState == CTCallStateDialing || call.callState == CTCallStateIncoming
{
print("In call")
return true
}
}
}
print("No calls")
return false
}
func cameBackFromSleep()
{
self.viewWillAppear(true)
}
override func viewWillAppear(_ animated: Bool)
{
print("is on call", isOnPhoneCall())
switch isOnPhoneCall()
{
case true:
print("startBtnAnimation")
startBtnAnimation()
recordBtnIsPressed = true
case false:
print("stopBtnAnimation")
stopBtnAnimation()
recordBtnIsPressed = false
default: break
}
}
Now it works fine. Not sure why CTCallCenter works so weird in AppDelegate.

CLLocationManager startUpdatingLocation() doesn't work when called in background

I have a particular use case where I want to be able to start location updates when the app is already in the background. Specifically, I want to be able to press a button on my Pebble watchapp, and that causes my companion iOS app to begin location updates.
I am able to successfully start the location updates from my Pebble only if the iOS app is either in the foreground or has just entered the background within the past few seconds (like 5?). After a few seconds has passed with the app in the background, I can no longer start the updates. I know that the updates haven't started, since the backgroundTimeRemaining starts counting down to 0 (and if the updates did start, it stays at the max value). I also have the blue bar at the top that shows when location updates are on ("_____ is Using Your Location") and that also fails to show up after this weird ~5 second threshold has passed.
I've looked at almost all of the related questions on SO, and I've already tried basically everything that's been suggested, such as starting a background task right before starting the location updates. I've also tried delaying the start of the location updates by a couple seconds after starting the background task, and that doesn't work either.
The strange thing is, I also have an Apple Watch app that does the same things as the Pebble app, and the Apple Watch is able to start the location updates without fail all the time. Maybe the Apple Watch can do something special where it unsuspends the iOS app?
This is on iOS 9.
Any kind of help is appreciated, I'll try anything that's suggested.
Update: I put together a quick sample app that demonstrates the issue.
The code is posted below, or you can download the project from GitHub:
https://github.com/JessicaYeh/BackgroundLocationTest
If you try it out, you can see that setting the delay to 2 seconds works, but when the delay is longer (like 10 seconds), the updates don't start.
//
// AppDelegate.swift
// BackgroundLocationTest
//
// Created by Jessica Yeh on 2/18/16.
// Copyright © 2016 Yeh. All rights reserved.
//
import UIKit
import CoreLocation
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let locationManager = CLLocationManager()
var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
var timer: NSTimer?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
debugPrint(__FUNCTION__)
// Setup location manager
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.allowsBackgroundLocationUpdates = true
return true
}
#objc func checkBackgroundTimeRemaining() {
debugPrint(UIApplication.sharedApplication().backgroundTimeRemaining)
}
#objc func endBackgroundTask() {
if self.backgroundTask != UIBackgroundTaskInvalid {
UIApplication.sharedApplication().endBackgroundTask(self.backgroundTask)
self.backgroundTask = UIBackgroundTaskInvalid
}
}
func applicationWillResignActive(application: UIApplication) {
debugPrint(__FUNCTION__)
}
func applicationDidEnterBackground(application: UIApplication) {
debugPrint(__FUNCTION__)
// Cancel old timer and background task
timer?.invalidate()
endBackgroundTask()
// Start a new background task
backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler {
self.endBackgroundTask()
}
// Start updating location
// let delaySec = 2.0 // successfully starts location update
let delaySec = 10.0 // doesn't start location update
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delaySec * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue(), {
debugPrint("Attempting to start updating location")
self.locationManager.startUpdatingLocation()
})
// Keep checking every 3 sec how much background time app has left
timer = NSTimer.scheduledTimerWithTimeInterval(3.0, target: self, selector: Selector("checkBackgroundTimeRemaining"), userInfo: nil, repeats: true)
}
func applicationWillEnterForeground(application: UIApplication) {
debugPrint(__FUNCTION__)
}
func applicationDidBecomeActive(application: UIApplication) {
debugPrint(__FUNCTION__)
}
func applicationWillTerminate(application: UIApplication) {
debugPrint(__FUNCTION__)
}
}
extension AppDelegate: CLLocationManagerDelegate {
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
debugPrint(__FUNCTION__)
}
}
Well, I don't exactly what you tried, but make sure you've turned on the Location updates on your target's Capabilities (Capabilities -> Background Modes) and also, for iOS 9.0, add this:
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"9.0")) {
self.locationManager.allowsBackgroundLocationUpdates = YES;
}
and change the line:
locationManager.requestWhenInUseAuthorization()
to:
locationManager.requestAlwaysAuthorization().
Good luck!

How to work with iOS background modes?

I want to create an application similar to WhatsApp and Viber (maybe Skype), for iPhone starting from iOS 8.4, using swift 2.2. So I need to send https requests to my server every 30 seconds (I am using `NSURLSession), to show that my application is online and other contacts can see it. There is no problem with it if application is in active state, timer works perfectly and every 30 seconds. But I need application to work also in Inactive, Background, Suspended and Not running (if that even possible) modes, even if iPhone is sleeping. I just need to send little signal, thats all.
I already have this code:
func registerBackgroundTask() {
backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler {
[unowned self] in self.endBackgroundTask()
}
assert(backgroundTask != UIBackgroundTaskInvalid)
}
func endBackgroundTask() {
UIApplication.sharedApplication().endBackgroundTask(backgroundTask)
backgroundTask = UIBackgroundTaskInvalid
}
func reinstateBackgroundTask() {
if backgroundTask == UIBackgroundTaskInvalid {
registerBackgroundTask()
}
}
func functionToPerformOnBackground() {
requestToServer()
}
This functions are called by timer in AppDelegate.swift like this:
func applicationDidEnterBackground(application: UIApplication) {
//START BACKGROUND TIMER!!!
if !timerForBackgroundRunning {
timerForBackground = NSTimer.scheduledTimerWithTimeInterval(20, target: self, selector: "functionToPerformOnBackground", userInfo: nil, repeats: true)
registerBackgroundTask()
timerForBackgroundRunning = true
}
}
and
func applicationDidBecomeActive(application: UIApplication) {
if timerForBackgroundRunning {
timerForBackground.invalidate()
if backgroundTask != UIBackgroundTaskInvalid {
endBackgroundTask()
}
timerForBackgroundRunning = false
}
}
This code works perfectly, but when iPhone is going to sleep, it stops to send requests. To force code work again I have to go in application again and then press Home Button on iPhone. I have tested Viber and WhatsApp and I can say that Whatsapp is working similar, but Viber knows how to solve this problem.
Can somebody help me, please?
I will be thankful for any help, advice or anything.
P.S. I have already seen this tutorial: http://www.raywenderlich.com/92428/background-modes-ios-swift-tutorial
Is is great but not complete.

Swift how to use NSTimer background?

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("update"), userInfo: nil, repeats: true)
}
func update() {
println("Something cool")
}
}
It's ok for the Simulator ,I will get continuous "Something cool" through I tapped the home button. But it worked out when I debug the app with my iPhone. I did't get anything when I tapped the home button and make my app run background.
Someone told me I can play a long blank music to make my App run in the background.But I don't know how to play music in the background with swift #matt
You can use beginBackgroundTaskWithExpirationHandler to get some background execution time.
class ViewController: UIViewController {
var backgroundTaskIdentifier: UIBackgroundTaskIdentifier?
override func viewDidLoad() {
super.viewDidLoad()
backgroundTaskIdentifier = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
UIApplication.sharedApplication().endBackgroundTask(self.backgroundTaskIdentifier!)
})
var timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "update", userInfo: nil, repeats: true)
}
func update() {
println("Something cool")
}
}
Swift 3.0
backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(self.backgroundTaskIdentifier!)
})
This code was inspired by this answer, but I ported to swift.
This apparently only runs for 3 minutes on iOS 7+.
I'm willing to bet that you don't need to run a timer in the background, what you need is to know the time that has elapsed while you were suspended. That is a much better use of your resources than trying to play an empty sound file.
Use applicationWillResignActive and applicationDidBecomeActive to determine how much time has elapsed. First save the fireDate of your timer and invalidate it in applicationWillResignActive
func applicationWillResignActive(application: UIApplication) {
guard let t = self.timer else { return }
nextFireDate = t.fireDate
t.invalidate()
}
Next, determine how much time is left for the timer in applicationDidBecomeActive by comparing the time now to the fireDate you saved in the applicationWillResignActive call:
func applicationDidBecomeActive(application: UIApplication) {
guard let n = nextFireDate else { return }
let howMuchLonger = n.timeIntervalSinceDate(NSDate())
if howMuchLonger < 0 {
print("Should have already fired \(howMuchLonger) seconds ago")
target!.performSelector(selector!)
} else {
print("should fire in \(howMuchLonger) seconds")
NSTimer.scheduledTimerWithTimeInterval(howMuchLonger, target: target!, selector: selector!, userInfo: nil, repeats: false)
}
}
If you need a repeating one, just do math to figure out how many times the timer should have fired while in the background
let howManyTimes = abs(howMuchLonger) / repeatInterval
when I tapped the home button and make my app run background
No, that's a false assumption. When you tap the home button, you do not make your app run in the background. You make your app suspend in the background. Your code does not run when you are in the background.
Apps can get special permission to run in the background just in order to perform certain limited activities, like continuing to play music or continuing to use Core Location. But your app does none of those things, so it goes into the background and stops.
add this code in appdelegate for run a task in background , its working finely,
var backgroundUpdateTask: UIBackgroundTaskIdentifier = 0
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
return true
}
func applicationWillResignActive(application: UIApplication) {
self.backgroundUpdateTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
self.endBackgroundUpdateTask()
})
}
func endBackgroundUpdateTask() {
UIApplication.sharedApplication().endBackgroundTask(self.backgroundUpdateTask)
self.backgroundUpdateTask = UIBackgroundTaskInvalid
}
func applicationWillEnterForeground(application: UIApplication) {
self.endBackgroundUpdateTask()
}

Resources