Unexpected crash when ApplicationDidEnterBackground: NSTimer - swift

Yesterday, I've ask because I have a problem with my timer in background, I've found a solution to make it works but now, I got a problem when my application enter in background.
Run in background:
backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(self.backgroundTaskIdentifier!)
})
This is my timer:
let interval = 0.01
timer = Timer.scheduledTimer(timeInterval: interval, target: self, selector:#selector(ViewController.updateTimer), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
This is my updateTimer function:
func updateTimer () {
var j = 0
for _ in rows {
if (rows[j]["Playing"] as! Bool == true ) {
rows[j]["time"] = (rows[j]["time"] as! Double + interval) as AnyObject
rows[j]["lastTime"] = (rows[j]["lastTime"] as! Double + interval) as AnyObject
}
if (rows[j]["lastTime"] as! Double > 60.0) {
min[j] += 1
rows[j]["lastTime"] = 0.00 as AnyObject
}
j += 1
}
}
In my applicationDidEnterBackground method I call this function:
func backgroundTimer() {
print("background") // This Print works fine
timer.invalidate() // Never works
interval = 1.00 // Crash when interval change from 0.01 to 1.00
timer = Timer.scheduledTimer(timeInterval: interval, target: self, selector:#selector(ViewController.updateTimer), userInfo: nil, repeats: true)
}
And this is my output when my application enter in background:
The Double is interval.
In Info.plist I add : Application does not run in background : NO.
Let me know what i'm doing wrong?
EDIT:
Initialization of rows in viewDidLoad method
let i: [String : AnyObject] = ["time": time as AnyObject, "Playing": false as AnyObject, "lastTime": 0.00 as AnyObject, "lapNumber": 0 as AnyObject, "min": 0 as AnyObject]
rows.append(i as [String : AnyObject])

Your basic problem seems to be the use of AnyObject for things that are not really objects. It causes the as! Bool to fail.
Here's a playground snippet that gets back a stored Bool value by allowing simple values in the dictionary.
var rows: [[String : Any]] = []
let i: [String : Any] = ["time": time, "Playing": false, "lastTime": 0.00, "lapNumber": 0, "min": 0]
rows.append(i)
let playing = rows[0]["Playing"]
if let playing = playing as? Bool {
print("Bool")
} else {
print("Something else \(String(describing: playing))")
}

I found solution for this problem.
Using NSNotification.
override func viewWillAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(self, selector:#selector(ViewController.backgroundTimer), name:NSNotification.Name.UIApplicationDidEnterBackground, object: nil)
NotificationCenter.default.addObserver(self, selector:#selector(ViewController.backToForeground), name:NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
}
func backgroundTimer(notification : NSNotification) {
self.timer.invalidate()
interval = 1.00
timer = Timer.scheduledTimer(timeInterval: interval, target: self, selector:#selector(ViewController.updateTimer), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
}
func backToForeground(notification: NSNotification) {
self.timer.invalidate()
interval = 0.01
timer = Timer.scheduledTimer(timeInterval: interval, target: self, selector:#selector(ViewController.updateTimer), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
}
Don't forgot to add the backgroundTaskIdentifier.

I suppose you are using the AppDelegate mainVC method that create a copy of your main. Thats why your timer are never invalidate in your ViewController. It is invalidate in your copy.
Check out this answer: Pausing timer when app is in background state Swift

Related

Timer failure in UNUserNotificationCenter.getNotificationSettings

I'm trying to start some timers on a view controller, based on whether or not the application has provided push permissions, but the timers are not triggering. Can anyone explain to me why that is?
class SomeViewController: UIViewController {
private var startTime: Date!
private var countDown: Timer?
private var timer: Timer?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startTime = Date()
UNUserNotificationCenter.current().getNotificationSettings { settings in
if settings.authorizationStatus == .denied || settings.authorizationStatus == .notDetermined {
self.timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(self.manualFunction(_:)), userInfo: nil, repeats: true)
} else {
// Wait two seconds, then start the manual check.
self.countDown = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { (timer) in
self.timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(self.manualFunction(_:)), userInfo: nil, repeats: true)
})
}
}
}
#objc private func manualFunction(_ timer: Timer) {
// Some function I want to execute whenever the second timer triggers.
}
}
The viewDidAppear function used to contain the following code that did work:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startTime = Date()
self.countDown = Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { [weak self] (timer) in
guard let self = self else { return }
self.timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(self.manualFunction(_:)), userInfo: nil, repeats: true)
})
}
I've tried making the timers weak, I've tried adding [unowned self] and [weak self] in several spots, without a success. Can someone explain this behaviour?
When calling the getNotificationSettings method you're entering the background queue and you should come back to your main queue to handle task that are to be performed in the main queue.
UNUserNotificationCenter.current().getNotificationSettings { settings in
DispatchQueue.main.async {
// Add your tasks here
}
}

Timer will not Invalidate swift 4

I see a couple posts referring to this issue where starting multiple timers might be the problem. However, I do not see myself starting more than one, maybe I am just missing something? I am very new at this. Right now I am trying to start the timer if the user is going faster than 8 mps, and keep it running until he has stopped long enough for the timer to run out. Currently the timer keeps counting down even after the conditions are met to invalidate it.
Thanks a ton for looking, the help here is always super appreciated
import UIKit
class ViewController: UIViewController, {
//Setup timer
var timer = Timer()
var seconds = 5
var timerRunning = false
//Timer countdown
#objc func timeoutPeriod() {
seconds -= 1
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if userLocation.speed > 8 && timerRunning == false {
//flip running to True and start timer
timerRunning = true
seconds = 10
Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(timeoutPeriod)), userInfo: nil, repeats: true)
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
let placemark: CLPlacemark = placemarks![0]
if let city = placemark.locality {
self.startingLocation = city
print("Starting Location: " + city)
}
})
} else if userLocation.speed > 8 && timerRunning == true {
seconds = 10
} else if seconds == 0 {
//end timer (hopefully)
self.timer.invalidate()
geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
let placemark: CLPlacemark = placemarks![0]
if let city = placemark.locality {
self.currentDateString = self.dateFormatter.string(from: self.date)
self.endingLocation = city
print("Ending Location: " + city)
self.timerRunning = false
self.seconds = 10
}
})
}
}
}
The normal timer function is not getting invalidated even if it is made nil but the below function helped me to solve the purpose. Hope this helps
self.myTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { (timerValue) in
if self.myTimer != nil {
self.updateTimerLabel() // call the selector function here
}
})
Your problem lies in this part of your code.
Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(timeoutPeriod)), userInfo: nil, repeats: true)
You're creating a Timer but not setting it into the Timer variable you created var timer = Timer()
To fix this you just have to set your Timer variable correctly.
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(timeoutPeriod)), userInfo: nil, repeats: true)
Why in my case:
First at DidLoad() ran slowly and stopped at increasInt = 9.
Second time triggered by code to make it automatically: reset increasInt = -1 - it ran faster
and continues after increasInt > 11
func aniDrag(ani: Double){
dragTimer = Timer.scheduledTimer(timeInterval: ani, target: self, selector: #selector(updatedrag), userInfo: nil, repeats: true)
}
#objc func updatedrag() { // dauIndex
increasInt = increasInt + 1
if (increasInt > -1 ) && ( increasInt < 10 ) {
aniImage.image = UIImage(named: "ani" + allLessonArray[realnIndex] + String(increasInt))!
print(allLessonArray[realnIndex] + String(increasInt) )
}
if increasInt == 10 {
if dragTimer != nil {
print("Timer Stop !!!!!")
dragTimer.invalidate()}
}
if increasInt > 11 { print(increasInt)}
}
//test aniDrag(ani: 0.5)

Swift Timer error when function has arguments

I'm trying to make a function that activates every one second, which adds 1 to a variable (texttimer) and adds a letter to a string (typedStory), which is displayed on a label (storyLabel).
Everything worked fine until I added an argument (finalStory) to the function. Now I'm getting an an error:
-[__NSCFTimer substringToIndex:]: unrecognized selector sent to instance
Here's the code I have:
func textTypeWelcome(finalStory: NSString){
var newTimer2 = Timer.scheduledTimer(timeInterval: 0.05, target: self, selector: #selector(GameScene.textTypeWelcome), userInfo: finalStory, repeats: false)
texttimer += 1
typedStory = finalStory.substring(to: texttimer)
storyLabel.text = typedStory
}
The code works, however doesn't do what I want if I remove the argument and put userInfo to nil.
Anyone know of a solution? Thanks!
No, that cannot work, the action method takes the timer as parameter – what the error message tells you – and you get finalStory from the userInfo parameter.
func textTypeWelcome(timer: Timer) {
let finalStory = timer.userInfo as! String
var newTimer2 = Timer.scheduledTimer(timeInterval: 0.05, target: self, selector: #selector(GameScene.textTypeWelcome), userInfo: finalStory, repeats: false)
texttimer += 1
typedStory = finalStory.substring(to: texttimer)
storyLabel.text = typedStory
}
Try this out in a Playground
class Foo : NSObject {
let string = "Hello World"
var counter = 1
var typedStory = ""
var timer = Timer()
override init() {
super.init()
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true)
print("timer started")
}
func timerFired(timer : Timer) {
typedStory = string.substring(to: string.index(string.startIndex, offsetBy: counter))
print(typedStory)
if typedStory == string {
timer.invalidate()
print("timer stopped")
} else {
counter += 1
}
}
}
let foo = Foo()
You have to add the lines
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
to allow asynchronous API

The timer is terminated when the view changes

I am not a native English speaker and I am writing with a translator.
I want you to understand if the context is strange.
My Question is,
When the button is pressed, the timer operates, but when the view is switched, the timer is not operating.
I wonder how the timer can be maintained even if the view changes.
var time : Int = 0
var onehour : Int = 60
var timer = Timer()
#IBAction func pressSuncream(_ sender: Any) { // Suncream 버튼
useSuncream.setImage(UIImage(named: "test"), for: UIControlState.normal)
if #available(iOS 10.0, *) {
let content = UNMutableNotificationContent()
content.title = "hello"
content.body = "timer"
content.badge = 1
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3600, repeats: false)
let request = UNNotificationRequest(identifier: "timerdone", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(QuestView.updatetime), userInfo: nil, repeats: true)
} else {
// Fallback on earlier versions
}
}
func updatetime() {
time += 1
if time == 60 {
time = 0
onehour -= 1
DispatchQueue.main.async {
self.suncreamTime.text = String(self.onehour)
}
}
}
when you switch to the mainView,you can store the value of onehour in the UserDefaults at the override func viewWillDisappear(_ animated: Bool) { }.
when you into the timerView,you can get the value at the override func viewWillAppear(_ animated: Bool) {}
eg:
override func viewWillDisappear(_ animated: Bool) {
let userDefault = UserDefaults.standard
userDefault.set(onehour, forKey: "onehour")
}
override func viewWillAppear(_ animated: Bool) {
if let onehour = UserDefaults.standard.value(forKey: "onehour") {
self.onehour = onehour as! String
}
}
One way that you could do this, is to make the timer variable global. This is probably a "dirty" way to do it, and if there really are terrible ramifications of doing this, someone let me know and I'll take this down.
Essentially all you would need to do is define it outside of any class like so:
var timer: NSTimer?
class MyClass: UIViewController {
...
}
This way you can access it from any file. I would say that you are generally discouraged from doing this unless you have to, so it's more of a last-resort option.

Pause NSTimer on home-button-press and start them again once the app is in foreground

I've been searching for a solution to pause my SpriteKit game when the user "tabs down" the game. So far I found a solution where you use SKAction's instead of NSTimer's, this works as long as the time between actions stays the same. However, my NSTimer's changes in speed. So I need to find another solution.
I have a bunch of NSTimer's located in GameScene -> didMoveToView
NSTimer.scheduledTimerWithTimeInterval(0.2, target: self, selector: Selector("SpawnBullets"), userInfo: nil, repeats: true)
NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("SpawnMeteors"), userInfo: nil, repeats: true)
NSTimer.scheduledTimerWithTimeInterval(timeInterval, target: self, selector: Selector("onTimer:"), userInfo: nil, repeats: true)
Now, how would I simply pause them when the app enters background?
EDIT: Added my timeInterval increase-speed-function
func onTimer(timer: NSTimer) {
var goodTimes = time / 20
if (goodTimes > 1.8){
goodTimes = 1.8
}
timer.fireDate = timer.fireDate.dateByAddingTimeInterval(timeInterval - goodTimes)
self.runAction(SKAction.sequence([SKAction.runBlock(SpawnRocks), SKAction.waitForDuration(goodTimes / 2), SKAction.runBlock(SpawnPowerUp)]))
}
Pausing of NSTimer is not a native feature in objective-c or swift. To combat this, you need to create an extension, which I happen to have created and will share for you. This will work for both OS X and iOS
import Foundation
import ObjectiveC
#if os(iOS)
import UIKit
#else
import AppKit
#endif
private var pauseStartKey:UInt8 = 0;
private var previousFireDateKey:UInt8 = 0;
extension NSTimer
{
private var pauseStart: NSDate!{
get{
return objc_getAssociatedObject(self, &pauseStartKey) as? NSDate;
}
set(newValue)
{
objc_setAssociatedObject(self, &pauseStartKey,newValue,objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN);
}
}
private var previousFireDate: NSDate!{
get{
return objc_getAssociatedObject(self, &previousFireDateKey) as? NSDate;
}
set(newValue)
{
objc_setAssociatedObject(self, &previousFireDateKey,newValue,objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN);
}
}
func pause()
{
pauseStart = NSDate();
previousFireDate = self.fireDate;
self.fireDate = NSDate.distantFuture() ;
}
func resume()
{
if(pauseStart != nil)
{
let pauseTime = -1 * pauseStart.timeIntervalSinceNow;
let date = NSDate(timeInterval:pauseTime, sinceDate:previousFireDate );
self.fireDate = date;
}
}
}
Then when you need to use it, simply call timer.pause() and timer.resume() You of course have to keep track of your timers in your gamescene object to do this, so make sure that timer is a variable accessible for the entire class, and when making your timer, you do timer = NSTimer.schedule...
At the beginning of your class:
var spawnBulletsTimer : NSTimer?;
var spawnMeteorsTimer : NSTimer?;
var onTimer: NSTimer?;
When creating the timers:
spawnBulletsTimer = NSTimer.scheduledTimerWithTimeInterval(0.2, target: self, selector: Selector("SpawnBullets"), userInfo: nil, repeats: true)
spawnMeteorsTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("SpawnMeteors"), userInfo: nil, repeats: true)
onTimer = NSTimer.scheduledTimerWithTimeInterval(timeInterval, target: self, selector: Selector("onTimer:"), userInfo: nil, repeats: true)
Then when you need to pause:
onTimer?.pause()
spawnBulletsTimer?.pause()
spawnMeteorTimer?.pause()
Then when you need to resume:
onTimer?.resume()
spawnBulletsTimer?.resume()
spawnMeteorTimer?.resume()
Thanks #KnightOfDragon code, here's a more modern swift version:
import Foundation
private var pauseStartKey:UInt8 = 0;
private var previousFireDateKey:UInt8 = 0;
extension Timer{
private var pauseStart: Date!{
get{
return objc_getAssociatedObject(self, &pauseStartKey) as? Date
}
set(newValue){
objc_setAssociatedObject(self, &pauseStartKey,newValue,objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
private var previousFireDate: Date! {
get{
return objc_getAssociatedObject(self, &previousFireDateKey) as? Date
}
set(newValue){
objc_setAssociatedObject(self, &previousFireDateKey,newValue,objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
func pause() {
pauseStart = Date()
previousFireDate = fireDate
fireDate = Date.distantFuture
}
func resume() {
if pauseStart != nil {
let pauseTime = -1 * pauseStart.timeIntervalSinceNow;
let date = Date(timeInterval: pauseTime, since: previousFireDate)
fireDate = date;
}
}
}