Keeping timers running when app enters background - ios

I am making an app that relies on a timer that fires every minute to change a label from "x minutes left" to "(x-1) minutes left. Basically, it decrements the number every minute. I need the timer to function even when I close the app (not entirely, just press the home screen and leave it running in the background), so that when the user comes back to the app after leaving it in the background for 5 minutes, the label will say "(x-5) minutes left".
Right now, when I run it in the simulator it works perfectly, but when I run it on my phone it does not work. If I have the app open on my phone, it works, but if the app is running in the background, the label never decrements. Could this be due to differences in the way the simulator and actual iPhone handle multitasking? If so, how can I change my code so that the timer will still update the label every minute so that the correct number is displayed when the user reopens the app?
Here is my timer setup:
var individualTaskTimer = NSTimer()
func createTimerForTopTask(){
individualTaskTimer = NSTimer.scheduledTimerWithTimeInterval(tasks[0].minutes * 60, target: self, selector: "deleteTopTask", userInfo: nil, repeats: true)
}

As mentioned in Ewan Mellor's answer, you will not be able to rely on a timer while the app is in the background. So you will need to adjust as necessary when your app returns to the foreground.
Upon first reading the documentation, it might seem like viewWillAppear and viewWillDisappear (or viewDidDisappear) are the correct places to handle this. However, they do not get called when the app moves to/from the background.
Instead, you can make use of two notifications, UIApplicationWillResignActiveNotification and UIApplicationDidBecomeActiveNotification. The first notification will be sent to your app when it is about to go into the background. The second notification will be sent to your app when it is about to return to the foreground.
So in viewWillAppear you can register for the notifications as follows:
override func viewWillAppear(animated: Bool) {
// some other code
NSNotificationCenter.defaultCenter().addObserver(self, selector: "activeAgain", name: "UIApplicationDidBecomeActiveNotification", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "goingAway", name: "UIApplicationWillResignActiveNotification", object: nil)
}
where activeAgain and goingAway are two functions you've written to enable and disable the timer. So based on the code snippet in your question, they would look something like this:
func activeAgain() {
let newTime = // calculate how much time is left (in seconds)
individualTaskTimer = NSTimer.scheduledTimerWithTimeInterval(newTime, target: self, selector: "deleteTopTask", userInfo: nil, repeats: true)
}
func goingAway() {
individualTaskTimer.invalidate()
}
Note that you need to unregister for the notifications when you switch away from this view. Doing this in viewWillDisappear is probably a good spot.

You can't do this. iOS will suspend your app after a short while when it goes into the background. Just update your label with the correct time when the app comes back to the foreground.

Related

Detect when app is closed, terminated but ignore when the phone is turned off

I want to know when someone closes my app (taps the home button) or terminates my app (double tap swipe up) however I do not want to know when the user is using my app and simply turns their phone off because when they turn their phone back on it will still be on my app.
I have tried using the applicationWillResignActive, applicationDidEnterBackground, applicationWillTerminate and registering it in my view controller
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.willResignActiveNotification, object: nil)
These either didn't tell me when my app wasn't the focus or if they did work they also told me when my app was still open just the phone was closed.
You cannot draw the distinction you are asking to draw. Whether the user clicks the Home button or turns off the screen, your app is deactivated and backgrounded and you are told so — and that is all you are told.
However, you do not need to draw this distinction. When your app is backgrounded, just do whatever is appropriate. You’ll be told when your app is foregrounded again, even if that is just because the user turned the screen back on.
So after some research the only possible way I found was:
func DidUserPressLockButton() -> Bool
{
let oldBrightness = UIScreen.main.brightness
UIScreen.main.brightness = oldBrightness + (oldBrightness <= 0.01 ? (0.01) : (-0.01))
return oldBrightness != UIScreen.main.brightness
}
func applicationDidEnterBackground(_ application: UIApplication)
{
if (DidUserPressLockButton())
{
//User pressed lock button
}
else
{
//user pressed home button
}}
This method works by seeing if the app can change the brightness however some say this will get rejected by apple because they don't want you to do which makes sense why there isn't a built in function for it however there are apps on the App Store that know how you left the app so there you go

Block screen recoding but allow HDMI screen mirroring Swift 4

I need to stop Screen recoding, however I need to allow video sharing through HDMI.
I know the captured notification is usually used for this, but I cant find a way to separate out these two things.
UIScreen.main.addObserver(self, forKeyPath: "captured", options: .new, context: nil)
The above triggers notifications when screen recording is started / stopped. But also triggers when an external display is connected / disconnected (though I'm finding it to be intermittent here).
I have tried using the following to detect if a screen within the UIScreen is mirrored or just to check the screen count. Recordings show only one screen in the count, and mirrored screens should show a count of 2
recordingLabel.text = "count \(UIScreen.screens.count)"
for screen in UIScreen.screens {
if screen.mirrored != nil {
recordingLabel.text = "Mirrored - count \(UIScreen.screens.count)"
}
}
But again this is intermittent. Most of the time the count does not change on connecting a HDMI screen, but does change to 2 on disconnecting.
I have also found using UIScreen notifications work for connected / disconnected. But there seems to be a race condition happening with the captured notification that I still need to handle recordings.
NotificationCenter.default.addObserver(self, selector: #selector(connected), name: UIScreen.didConnectNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(disconnected), name: UIScreen.didDisconnectNotification, object: nil)
Is there any way to detect the type of recording or prevent the race from happening when both types of notifications trigger?
EDIT :
I have since found
NotificationCenter.default.addObserver(self, selector: #selector(captureChanged(notification:)), name: UIScreen.capturedDidChangeNotification, object: nil)
Which works better with the connection notifications, but it still triggers before I get a connected notification. I don't really want to add a timer to detect if the didConnect notification triggers after the captureChange, but that might be the only way

How to prevent SpriteKit game from crashing when Notification Center is opened

I am having a weird issue with a SpriteKit game. The app crashes when a user opens Notification center or Control Center. I want the worldNode layer to be paused and the menuNode layer to be presented when the applicationWillResignActive is called, which is working fine when the home button is pressed. I've tried pausing my game from within the AppDelegate functions applicationWillResignActive and I've tried pausing when applicationDidBecomeActive is called if the game has started. I've tried using the NotificationCenter approach from within the ViewController both work when the Home Button/Home Gesture is used. How Should I handle this? Or is this just a bug in iOS 11?
I have this in the viewDidLoad method of my ViewController
NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
Then each one of those selectors
It crashes on both if trying to present the menu when the appDidBecomeActive and appWillResignActive
#objc func appDidBecomeActive() {
print("appDidBecomeActive")
if GameScene.sharedInstance != nil {
print("presentingMenu")
GameScene.sharedInstance.presentMenuNode()
}
}
#objc func appWillResignActive() {
print("appWillResignActive")
if GameScene.sharedInstance != nil {
print("presentingMenu")
GameScene.sharedInstance.presentMenuNode()
}
}
I feel like I may be trying to approach this the wrong way, but what I don't understand is why does it work when the Home button/Home gesture is fired?
Edit:
After more testing I found that everything works as expected when running on iOS 10 devices. However when I run the DemoBots app that apple provides from their sample code it doesn't crash on iOS 11 and basically does what I want to do, so there has got to be a better way to handle the transitions of the game state, any input is appreciated.

Keep running NSTimer switching Background and Foreground

I am trying to create a NSTimer which is started manually by the user in foreground, and as expected works perfectly:
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateCounter"), userInfo: nil, repeats: true)
where updateCounter() simply updates a UILabel in the UI.
Now what I want to achieve is to get it running also when the user leaves the application. I googled something and found this code (which I translated in Swift):
func applicationDidEnterBackground(application: UIApplication) {
var bgTask = UIBackgroundTaskIdentifier()
bgTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler { () -> Void in
UIApplication.sharedApplication().endBackgroundTask(bgTask)
}
// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
var vc = TimerViewController()
// Fire some methods
UIApplication.sharedApplication().endBackgroundTask(bgTask)
bgTask = UIBackgroundTaskInvalid
});
}
Basically from what I can understand here is that when app reaches background I can fire some methods from a new instance of my TimerViewController(), which is supposed to handle the NSTimer for the background phase.
Now does this imply that I need to use a method from this vc, or another, which starts another NSTimer starting from the exact time in which the foreground-one was stopped (because app entered background)?
If yes, to resume my first NSTimer when the user opens the app again (meaning foreground again :D) how should I behave? Stop the second NSTimer and resume the first with updated counter?
Actually even a sketch of the solution is appreciated, I'm just trying to figure out how to handle the situation.
further question: If what it's written above turn out to be correct how can I manage the instances of the viewcontroller to get the timer_value(token for something like secondsLeft) when a switch from background to foreground or vice-versa happens?
Instead of trying to maintain an NSTimer in the background (which is impossible beyond about 10 minutes), what you want to do is record when your timer started and save that in NSUserDefaults. You can still update your timer every second, but just recalculate the label based on the start time (remember, NSTimer doesn't promise that it will run on time; it could be late for many reasons and you'll drift).
Now there is no reason to run in the background. When your view controller comes onscreen (viewDidAppear:), just update the label again, based on the start time saved in NSUserDefaults.
There is no way in iOS to run indefinitely in the background. That's by design. But there's also no reason for you to be wasting system resources trying to update your UI when you're not onscreen. Just update it when you are.

Prevent SKNode from unpausing automatically when app resumes

In my game, I have functions to pause and unpause an SKNode that contains the gameplay elements. Currently, the system automatically pauses when the home button is pressed, and unpauses when the app becomes active again.
I would like to do this on my own terms. For instance, when the app becomes active again, it should show the pause menu, and stay paused until the user manually unpauses.
Is there a way to override this system behavior?
You can register to receive the UIApplicationDidEnterBackgroundNotification or UIApplicationWillEnterForegroundNotification notification, and trigger the pause menu in the notification selector.
Somewhere in your code, you'd have to register a notification. There are plenty of notifications to choose from. Here is one for when the App comes back from the background:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "didBecomeActive:", name: UIApplicationDidBecomeActiveNotification, object: nil)
}
func didBecomeActive(test: NSNotification) {
self.unpause()
}
Notice that the selector in the addObserver method is the name of the function that will be called when the application comes back from the background. Also the didBecomeActive method needs an argument of type NSNotification.

Resources