Swift Timer error when function has arguments - swift

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

Related

Why does my timer in swift keep speeding up?

I am creating a trivia app in swift and I have a timer that counts down each question. However as the user progresses with each question the timer speeds up. Can someone help me fix this?
My runGameTimer function:
func runGameTimer()
{
gameTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(RockTriviaViewController.updateGameTimer), userInfo: nil, repeats: true)
}
My updateGameTimer function:
#objc func updateGameTimer()
{
gameInt -= 1
timerLabel.text = String(gameInt)
if (gameInt == 0)
{
gameTimer.invalidate()
/*
if (currentQuestion != rockQuestions[questionSet].count)
{
newQuestion()
}
else
{
performSegue(withIdentifier: "showRockScore", sender: self)
}
*/
}
}
Where I call my code:
func newQuestion()
{
gameInt = 11
runGameTimer()
rockQuestion.text = rockQuestions[questionSet][currentQuestion]
rightAnswerPlacement = arc4random_uniform(3)+1
var Button: UIButton = UIButton()
var x = 1
for i in 1...3
{
Button = view.viewWithTag(i) as! UIButton
if(i == Int(rightAnswerPlacement))
{
Button.setTitle(rockAnswers[questionSet][currentQuestion][0], for: .normal)
}
else
{
Button.setTitle(rockAnswers[questionSet][currentQuestion][x], for: .normal)
x = 2
}
}
currentQuestion += 1
}
You're calling runGameTimer() in every call to newQuestion(). If a timer was already running then you'll add a new timer each time, and they will all call your selector. So if you have 3 timers running, your selector will be called 3x as often. That's not what you want.
Change your timer variable to be weak:
weak var gameTimer: Timer?
And then in runGameTimer invalidate the timer before creating a new one, using optional chaining:
func runGameTimer() {
gameTimer?.invalidate() //This will do nothing if gameTimer is nil.
//it will also cause the gameTimer to be nil since it's weak.
gameTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(RockTriviaViewController.updateGameTimer), userInfo: nil, repeats: true)
}
By making the game timer weak it will get set to nil as soon as it's invalidated. (When you schedule a timer the system retains it while it is running so it stays valid as long as it continues to run.)
By using optional chaining to reference the timer:
gameTimer?.invalidate()
The code doesn't do anything if gameTimer is nil.

Unexpected crash when ApplicationDidEnterBackground: NSTimer

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

Argument of '#selector' does not refer to an '#objc' method, property or initializer

Can anyone tell me why this code gives the error message "Argument of '#selector' does not refer to an '#objc' method, property or initializer"?
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:#selector(updateTimer(until: 3)), userInfo: nil, repeats: true)
Here's the function:
func updateTimer(until endTime: Int) {
counter -= 1
timeLabel.text = String(counter)
if counter == endTime {
step += 1
}
}
What I have tried:
1. Adding #objc in front of the function.
The selector of a target / action method must be declared either without parameter or with one parameter passing the affected object.
In case of a Timer use the userInfo parameter to pass data.
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:#selector(updateTimer(_:)), userInfo: 3, repeats: true)
func updateTimer(_ timer: Timer) {
let endTime = timer.userInfo as! Int
counter -= 1
timeLabel.text = String(counter)
if counter == endTime {
step += 1
}
}
If the enclosing class does not inherit form NSObject you have to add the #objc attribute to the action method.

Timer on swift not working

I have a function working with a button:
#IBAction func btnSiguiente(_ sender: Any) {
if indexImagenes == imagenes.count {
indexImagenes = 0
}
escaparate.image = NSImage(named: imagenes[indexImagenes])
indexImagenes += 1
}
I want to make it work with a Timer:
var playTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(autoplay), userInfo: nil, repeats: true)
func autoplay() {
if indexImagenes == imagenes.count {
indexImagenes = 0
}
escaparate.image = NSImage(named: imagenes[indexImagenes])
indexImagenes += 1
}
But I get this in console:
[_SwiftValue autoplay]: unrecognized selector sent to instance 0x6080000451c0
To fix this, initialize the timer once the instance of the class your are in is fully initialized.
So just give the var playTimer a default value as follows:
var playerTimer:Timer? = nil
Then initialize the timer in some function that gets called once 'self' is fully initialized (like viewDidLoad() for example, assuming this code is in a viewController):
override func viewDidLoad()
{
super.viewDidLoad()
playerTimer = Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(autoplay), userInfo: nil, repeats: true)
}
Essentially just make sure 'self' is fully initialized before you send it a selector to perform.

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;
}
}
}