Something really odd is happening with my code.
I made a rather simple Timer function that is triggered by button.
The button calls a first method, then the method use another function to do the counting, then it triggers something when the time's up.
And everything works fine.
here's the code.
// This is the declaration of the launch button.
#IBAction func playLater(_ sender: Any) {
if isTimerRunning == false {
runTimer()
}
}
var seconds = 10
var timer = Timer()
var isTimerRunning = false
var resumeTapped = false
//the timer function that sets up the duration and launches the counting.
func runTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(self.updateTimer)), userInfo: nil, repeats: true)
isTimerRunning = true
}
//this part is making sure that you go from 10 to 0 and when it's at 0, something happens. In this very case, it plays a song.
#objc func updateTimer() {
if seconds < 1 {
timer.invalidate()
isTimerRunning = false
playNow((Any).self)
} else {
seconds -= 1
timerLabel.text = timeString(time: TimeInterval(seconds))
timerLabel.text = String(seconds)
}
}
The thing is that I also want to be able to triger that same function from the Apple Watch.
I made a WCSession that is working well, the messages are passing, it's ok.
So I'm using this code to launch the same function when the user pushes a button on the apple Watch. That part of the code is on the same iOS swift file.
#available(iOS 9.0, *)
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
// do something
isTimerRunning = false
playLater(Any.self)
}
As you can see, I'm not even trying to add some code, I just call the same function used with the iOS button.
But this time, it's not working. The first part of the method is responding, I saw that runTimer() is working, but it's not going to updateTimer().
Maybe I'm missing something here, but, what is the difference ? Why if it comes from the push of a button it's working, and if it's called directly from "the inside" nothing happens ?
If you have any educated guesses, or even, clues, I'd be grateful.
Thanks !
Related
I'm learning how to create a Pomodoro app, and am able to send notifications. However, I am totally clueless as to how to allow my timer label to update itself on reloading the app. Which means the timer works only when the app is open and not when it's in the foreground/background. Hoping to find a tutorial to learn from or just a quick answer code. Thanks!
Edit: Just to clear some misunderstandings, my app's Notification works fine with the timer, for example if 30mins is selected, the app would notify the user after 30mins. However, the problem is that when the app reopens, it resumes for example 29:57 seconds left on the timer label while the 30mins should have passed already.
*Added in AppDelegate*
var seconds = 0 //Timer countdown seconds
var currentDate = NSDate()
var setDate: Int = 0
func pauseApp(){
viewC.timer.invalidate() //invalidate timer
UserDefaults.standard.set(seconds, forKey: "current") //error occurs here where "Cannot assign value of type NSDate to type Timer"
setDate = UserDefaults.standard.integer(forKey: "current")
}
func startApp(){
let difference = currentDate.timeIntervalSince(NSDate() as Date) as Double
seconds = Int(Double(setDate) - difference)
viewC.updateTimer()
}
What someone suggests from a different thread is cancel the timer and store a NSDate when the app goes to the background. He stated we can use this notification to detect the app going to the background:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseApp", name: UIApplicationDidEnterBackgroundNotification, object: nil)
Then cancel the timer and store the date:
func pauseApp(){
self.stop() //invalidate timer
self.currentBackgroundDate = NSDate()
}
Use this notification to detect the user coming back:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "startApp", name: UIApplicationDidBecomeActiveNotification, object: nil)
Then calculate the difference from the stored date to the current date, update your counter and start the timer again:
func startApp(){
let difference = self.currentBackgroundDate.timeIntervalSinceDate(NSDate())
self.handler(difference) //update difference
self.start() //start timer }
However, I do not fully understand this code (namely, the difference between the "handler" and my own "seconds") as am new to programming... Hoping for an answer or helpful insight.
Solved: I managed to solve it myself from this video https://www.youtube.com/watch?v=V6ta24iBNBQ
Using this concept of timeDifference as well as UserDefaults.standard.set....
I managed to adapt it to my personal app with the code
You can call Timer to run the timmer when the view loads.
var runTimer : Timer?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
runTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(myFun), userInfo: nil, repeats: true)
}
func myFun(){
//do your logic
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
runTimer?.invalidate()
}
I want to send a double vibrate when a countdown comes to an end, but all of the options I've seen don't work. For example, I've tried the following:
AudioServicesPlayAlertSound(UInt32(kSystemSoundID_Vibrate))
But it doesn't work as described. I can't remember if it only vibrates once or not at all, but it does not vibrate twice
let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
feedbackGenerator.impactOccurred()
let feedbackGenerator = UINotificationFeedbackGenerator()
feedbackGenerator.notificationOccurred(.error)`enter code here`
let feedbackGenerator = UISelectionFeedbackGenerator()
feedbackGenerator.selectionChanged()
None of those worked at all.
And finally I tried this:
var counter = 0
var timer : NSTimer?
func vibratePhone() {
counter++
switch counter {
case 1, 2:
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
default:
timer?.invalidate()
}
}
#IBAction func vibrate(sender: UIButton) {
counter = 0
timer = NSTimer.scheduledTimerWithTimeInterval(0.6, target: self, selector: "vibratePhone", userInfo: nil, repeats: true)
}
}`
This does work, but it has a lag of about 1 second prior to playing the first vibrate, which is better than nothing, but not what I'd like to ship with.
What would be ideal is if I could figure out how to use one of the vibrate patterns I created on my iPhone, then save it off to my MBP and use it in my Xcode app, but the closest I can come to this is getting the customer vibrate plist file off my phone. And after that I haven't a clue how to use it (if I can use it at all).
Does anyone have any ideas on how to achieve this?
Thanks!
I have a sliderValueChange function which updates a UILabel's text. I want for it to have a time limit until it clears the label's text, but I also want this "timed clear" action to be cancelled & restarted or delayed whenever the UISlider is moved within the time limit before the "timed clear" action takes place.
So far this is what I have:
let task = DispatchWorkItem {
consoleLabel.text = ""
}
func volumeSliderValueChange(sender: UISlider) {
task.cancel()
let senderValue = String(format: "%.2f", sender.value)
consoleLabel.text = "Volume: \(senderValue)"
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: task)
}
Obviously, this approach does not work, since cancel() apparently cannot be reversed.. (or at least I don't know how). I also don't know how to start a new task at the end of this function which will be cancelled if the function is recalled..
Am I going about this the wrong way? Is there something I am overlooking to make this work?
Use a timer:
weak var clearTimer: Timer?
And:
override func viewDidLoad() {
super.viewDidLoad()
startClearTimer()
}
func startClearTimer() {
clearTimer = Timer.scheduledTimer(
timeInterval: 3.0,
target: self,
selector: #selector(clearLabel(_:)),
userInfo: nil,
repeats: false)
}
func clearLabel(_ timer: Timer) {
label.text = ""
}
func volumeSliderValueChange(sender: UISlider) {
clearTimer?.invalidate() //Kill the timer
//do whatever you need to do with the slider value
startClearTimer() //Start a new timer
}
The problem is that you are cancelling the wrong thing. You don't want to cancel the task; you want to cancel the countdown which you got going when you said asyncAfter.
So use a DispatchTimer or an NSTimer (now called a Timer in Swift). Those are counters-down that can be cancelled. And then you can start counting again.
I have a problem with two timers running in the same View Controller. I need one of them to launch when the other is invalidated and go back on after the start button for the first is tapped again. I tried creating two variables and it builds successfully, but the behavior is erratic. What would be the right approach? Thanks
#IBAction func Start(_ sender: AnyObject) {
timer2.invalidate()
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(ViewController.update), userInfo: nil, repeats: true)
}
#IBAction func pauseTimer(_ sender: AnyObject) {
timer.invalidate()
timer2 = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: Selector(("increaseTimer")), userInfo: nil, repeats: true)
}
So you need a sort of "flip flop" timer effect?
Hav you thought about replacing var timerHasFinishedRunning: Bool = false with checking the invalidation?
e.g.
// When you invalidate, rather
timer1.invalidate()
timer1 = nil
// As goes for timer2
timer2.invalidate()
timer2 = nil
That way, you can have your timer checking done view computed properties:
var timer1HasFinishedRunning: Bool {
return self.timer1 == nil
}
var timer1HasFinishedRunning: Bool {
return self.timer2 == nil
}
Also you mention that they behave "erratically", could you elaborate? Your timer interval is 1 second, so if anything erratic happens, "within 1 second" it's probably because of the long interval. e.g. the checking of each is only done once per second, so sometimes could take as long as 1.999999 seconds to notice that a timer was invalidated.
Personally, I'd have the interval at 0.1 rather than 1.0 for greater accuracy.
I have a problem with invalidating different timers.
I have multiple timers (NSTimer) on a viewcontroller(settingsVC):
class settingsVC: UIViewController {
// I use 12 timers
var timer1 = NSTimer()
// Seconds to end the timer. Set 12 timers
let timeInterval1:NSTimeInterval = 10
var timer2 = NSTimer()
let timeInterval2:NSTimeInterval = 20
var timer3 = NSTimer()
let timeInterval3:NSTimeInterval = 30
//and so on ... 12 timers
}
With a UIButton (Start) a segue is performed. And for every different value of the variable 'picked', a different timer will be started in the same class:
class settingsVC: UIViewcontroller {
let defaults = NSUserDefaults.standardUserDefaults()
let pickerDefaultsIntegerKey = "Picker" // nsuserdefaults key
#IBAction func start(sender: AnyObject) {
// segue to another viewcontroller
performSegueWithIdentifier("timerOn", sender: self)
if picked == 1 {
defaults.setInteger(1, forKey: pickerDefaultsIntegerKey)
timer1 = NSTimer.scheduledTimerWithTimeInterval(timeInterval1,
target: self,
selector: "timerDidEnd:",
userInfo: nil,
repeats: false)
print("timer1 started")
} else if picked == 2 {
defaults.setInteger(2, forKey: pickerDefaultsIntegerKey)
timer2 = NSTimer.scheduledTimerWithTimeInterval(timeInterval2,
target: self,
selector: "timerDidEnd:",
userInfo: nil,
repeats: false)
print("timer2 started")
} else if // ....and so on{........ }
}
The method fired if timer ends, see selector:
func timerDidEnd(timer:NSTimer){
print("timer ended")
// do other stuff
}
I invalidate the timers with a button (Reset) for values from a variable ('pickerSavedSelection') which is updated by saved values in NSUserdefaults:
#IBAction func reset(sender: AnyObject) {
if let pickerSavedSelection = defaults.integerForKey(pickerDefaultsIntegerKey) as Int?
{
if pickerSavedSelection == 1 {
timer1.invalidate()
} else if pickerSavedSelection == 2 {
timer2.invalidate()
} else if //...and so on{....}
}
All goes well, if I outcomment the perform segue line and just let the user stay on this viewcontroller.The timers get invalidated correctly then:
In the console I read 'timer1 started' and I do NOT read 'timer ended' when the resetButton is pressed.
But staying on this viewcontroller(settingsVC) is NOT the flow of my app.
When the perform segue line is executed and the user 'comes back' to the viewcontroller (settingsVC), the timers are not invalidated when user presses the resetButton:
In the console I read 'timer1 started' and I DO read 'timer ended' when the resetButton is pressed.
How should I stop the timers, when users will 'exit' the viewcontroller and come back to reset the timers?
Help is much appreciated! Thanks in advance
If I am not mistaken at any given point in time, you are only triggering one NSTimer. All your different timers are differentiated only in time intervals. So, my suggestion would be to keep only one NSTimer and have your time interval differentiated. With different value picked you should first invalidate the timer and then restart it with new time interval. That said, your reset will then be much simplified and you do not need to save pickerSavedSelection in NSUserDefaults. This is how I would re-write this code:
class settingsVC: UIViewController {
var timer = NSTimer()
#IBAction func start(sender: AnyObject) {
// segue to another viewcontroller
performSegueWithIdentifier("timerOn", sender: self)
if picked == 1 {
self.timer.invalidate()
self.timer = NSTimer.scheduledTimerWithTimeInterval(10,
target: self,
selector: "timerDidEnd:",
userInfo: nil,
repeats: false)
print("timer1 started")
} else if picked == 2 {
self.timer.invalidate()
self.timer = NSTimer.scheduledTimerWithTimeInterval(20,
target: self,
selector: "timerDidEnd:",
userInfo: nil,
repeats: false)
print("timer2 started")
} else if // ....and so on{........ }
}
#IBAction func reset(sender: AnyObject) {
self.timer.invalidate()
}
}
PS: As a side note, I would advise your NSTimer to start & stop from main thread. Use GCD for that.
It is because your selector is not called when your timer is invalidated, it is called everytime your timer is fired. Since the timer is non-repeat, the selector get called only once. When your press reset button, timer is actually invalidated, you just didn't know because you misunderstood scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: method.