Stop and restart a timer - swift

I want to stop this timer and then restart it from where I stopped it.
secondsTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(addSeconds), userInfo: nil, repeats: true)
Below, it was suggested I shouldn't increment a timer in my timer handler. Why not?
For example, using GCD timer:
func countSeconds() {
secondsTimer = DispatchSource.makeTimerSource(queue: .main)
secondsTimer?.schedule(deadline: .now(), repeating: 1.0)
secondsTimer?.setEventHandler { [weak self] in
self?.addSeconds()
}
}
#objc func addSeconds() {
seconds += 1
}
func startGame() {
secondsTimer?.resume()
}

We don't pause/resume Timer instances. We stop them with invalidate(). And when you want to restart it, just create new timer.
Please refer to the Timer documentation, also available right in Xcode.
Note that you can suspend and resume GCD timers, DispatchSourceTimer.
var timer: DispatchSourceTimer? // note, unlike `Timer`, we have to maintain strong reference to GCD timer sources
func createTimer() {
timer = DispatchSource.makeTimerSource(queue: .main)
timer?.schedule(deadline: .now(), repeating: 1.0)
timer?.setEventHandler { [weak self] in // assuming you're referencing `self` in here, use `weak` to avoid strong reference cycles
// do something
}
// note, timer is not yet started; you have to call `timer?.resume()`
}
func startTimer() {
timer?.resume()
}
func pauseTiemr() {
timer?.suspend()
}
func stopTimer() {
timer?.cancel()
timer = nil
}
Please note, I am not suggesting that if you want suspend and resume that you should use GCD DispatchSourceTimer. Calling invalidate and recreating Timer as needed is simple enough, so just do that. I only provide this GCD information for the sake of completeness.
By the way, as a general principle, never "increment" some counter in your timer handler. That's a common mistake. Timers are not guaranteed to fire every time or with exact precision. Always save some reference time at the start, and then in your event handler, calculate differences between the current time and the start time. For example, extending my GCD timer example:
func createTimer() {
timer = DispatchSource.makeTimerSource(queue: .main)
timer?.schedule(deadline: .now(), repeating: 0.1)
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [.hour, .minute, .second, .nanosecond]
formatter.zeroFormattingBehavior = .pad
timer?.setEventHandler { [weak self] in
guard let start = self?.start else { return }
let elapsed = (self?.totalElapsed ?? 0) + CACurrentMediaTime() - start
self?.label.text = formatter.string(from: elapsed)
}
}
var start: CFTimeInterval? // if nil, timer not running
var totalElapsed: CFTimeInterval?
#objc func didTapButton(_ button: UIButton) {
if start == nil {
startTimer()
} else {
pauseTimer()
}
}
private func startTimer() {
start = CACurrentMediaTime()
timer?.resume()
}
private func pauseTimer() {
timer?.suspend()
totalElapsed = (totalElapsed ?? 0) + (CACurrentMediaTime() - start!)
start = nil
}

I do it with this code:
var timer: Timer?
func startTimer() {
timer = .scheduledTimer(withTimeInterval: 4, repeats: false, block: { _ in
// Do whatever
})
}
func resetTimer() {
timer?.invalidate()
startTimer()
}

You can start, stop and reset timer in swift4
class ViewController: UIViewController {
var counter = 0
var timer = Timer()
var totalSecond = 20
#IBOutlet weak var label1: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func start_btn(_ sender: Any) {
timer.invalidate() // just in case this button is tapped multiple times
timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
}
#IBAction func stop_btn(_ sender: Any) {
do {
self.timer.invalidate()
}
func timeFormatted(_ totalSeconds: Int) -> String {
let seconds: Int = totalSeconds % 60
return String(format: "0:%02d", seconds)
}
}
#IBAction func reset_btn(_ sender: Any) {
timer.invalidate()
//timerAction()
counter = 0
label1.text = "\(counter)"
}
#objc func timerAction()
{
counter += 1
label1.text = "\(counter)"
}
}

You can declare the Timer as 'weak var' instead of just 'var' like:
weak var timer: Timer?
Now you can pause your timer with:
timer?.invalidate()
To resume:
timer?.fire()

Related

timer doesn't work in cocoa/mac OS in Swift 4

I cant see why this time doesnt update. Been looking the entire day. I don't know if is something related to being my first MacOs project, and there's maybe something that's escaping me, but I'd love some help.
import Cocoa
class TextViewController: NSViewController {
#IBOutlet weak var text: NSScrollView!
#IBOutlet weak var dreadline: NSTextField!
var seconds: Int = 60
var timer: Timer?
var theWork = Dreadline(email: "", worktime: 0)
override func viewDidLoad() {
super.viewDidLoad()
print(seconds)
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print(self.seconds)
self.updateTimer()
} // this is the timer that doesn't work no matter what I try :(
}
#objc func updateTimer() {
if seconds < 1 {
timer?.invalidate()
} else {
seconds -= 1 //This will decrement(count down)the seconds.
dreadline.stringValue = "Dreadline: " + timeString(time: TimeInterval(seconds)) //This will update the label.
}
}
A very common mistake: You are creating a local timer which is not the same as the declared property.
Replace
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print(self.seconds)
self.updateTimer()
} // this is the timer that doesn't work no matter what I try :(
with
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print(self.seconds)
self.updateTimer()
} // this is the timer that doesn't work no matter what I try :(
The self before timer is actually not mandatory.
And set the timer to nil after invalidation to avoid a retain cycle
if seconds < 1 {
timer?.invalidate()
timer = nil
}
On the other hand you can use the local timer by deleting the property and change the code to
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print(self.seconds)
self.updateTimer(timer)
}
func updateTimer(_ timer : Timer) {
if seconds < 1 {
timer.invalidate()
} else {
seconds -= 1 //This will decrement(count down)the seconds.
dreadline.stringValue = "Dreadline: " + timeString(time: TimeInterval(seconds)) //This will update the label.
}
}

Unable to stop timer through another viewcontroller, Swift

I developing a timer app for an apple watch.
I have two different Views at the moment. One with the actual timer (TimerController) and another with a pause-button (SwipeController).
I'm trying to stop/start the timer in the TimerController with the action from the button in the SwipeController.
Problem is that the timer stops, but the timer will not start again after hitting the button the second time.
If I press the button one time, the timer stops. If i press it again two times the timer will start again but will not stop when hitting the button again.
Any ideas of what the problem could be?
TimeController
import WatchKit
import Foundation
import UserNotifications
class TimerController: WKInterfaceController {
#IBOutlet weak var timerOutlet: WKInterfaceTimer! //
#IBOutlet weak var simple_timer_label: WKInterfaceLabel!
var myTimer : Timer?
var duration : TimeInterval = 1 //arbitrary number. 1 seconds
var isPaused = false //flag to determine if it is paused or not
var elapsedTime : TimeInterval = 0.0 //time that has passed between
var number_as_a_timer:Int = 0
var startTime = NSDate()
var dim_date = Date()
var current_minute: Int = 0
var current_hour: Int = 0
var curent_second: Int = 0
var seperate_is_paused_bool: Bool = false
override func awake(withContext context: Any?) {
super.awake(withContext: context)
start_timer()
}
func timeString(time:TimeInterval) -> String {
let hours: Int = Int(time) / 3600
let minutes: Int = Int(time) / 60 % 60
let seconds: Int = Int(time) % 60
let com = NSDateComponents()
com.minute = minutes
com.second = seconds
com.hour = hours
dim_date = NSCalendar.current.date(from: com as
DateComponents)!
self.timerOutlet.setDate(dim_date)
self.timerOutlet.start()
return String(format:"%02i:%02i:%02i", hours, minutes, seconds)
}
func start_timer() {
myTimer = Timer.scheduledTimer(timeInterval: duration, target:
self,selector: #selector(timerDone), userInfo: nil, repeats:
true)
}
#objc private func timerDone(){
//timer done counting down
if !isPaused {
number_as_a_timer += 1
let output:String = self.timeString(time:
TimeInterval(number_as_a_timer))
self.simple_timer_label.setText(output)
print(output)
}
}
override func willActivate() {
super.willActivate()
NotificationCenter.default.addObserver(self, selector:
#selector(stop_timer(notification:)), name: .stopTimer, object:
nil)
}
#objc func stop_timer(notification:NSNotification) {
// Timer is paused. so unpause it and resume countdown
if isPaused {
myTimer = Timer.scheduledTimer(timeInterval: 1,
target:self, selector: #selector(timerDone), userInfo: nil,
repeats: true)
self.isPaused = false
print("timer paused: resumming1")
} else {
isPaused = true
print("stoping timer")
//get how much time has passed before they paused it
let paused = NSDate()
elapsedTime += paused.timeIntervalSince(startTime as Date)
//stop watchkit timer on the screen
timerOutlet.stop()
//stop the ticking of the internal timer
myTimer!.invalidate()
}
}
}
extension Notification.Name {
static let stopTimer = Notification.Name("stopTimer")
}
SwipeController
import WatchKit
import Foundation
import UserNotifications
class SwipeController: WKInterfaceController {
//#IBOutlet weak var myTimer: WKInterfaceTimer!
var timer = TimerController()
var status: Bool = false
override func awake(withContext context: Any?) {
super.awake(withContext: context)
}
#IBAction func PauseButton() {
if timer.myTimer == nil {
print("timer is nil or invalidated")
print("Y: \(timer.isPaused)")
let userInfo = ["stop": true] as [String: Bool] // you
could also transfer data
NotificationCenter.default.post(name: .stopTimer, object:
nil, userInfo: userInfo)
} else {
print("empty block")
}
}
}
it looks like you aren't ever actually checking for you isPaused boolean to be true or false in your if statement when checking if your timer is paused.
if isPaused { <-----------
myTimer = Timer.scheduledTimer(timeInterval: 1,
target:self, selector: #selector(timerDone), userInfo: nil,
repeats: true)
self.isPaused = false
print("timer paused: resumming1")

Timer scope issue with #selector

I'm stuck with getting this into the right scope. I'm sure its something super simple but I'm banging my head against a wall with it. any answers i'm finding are in earlier version of swift so im struggling to understand how to solve this
My current issue is trying to get the timer initialised correctly and counting. the "selector" is causing the most issues. the rest i'm sure ill be able to figure out afterwards
code is as follows.
#IBOutlet weak var shortTimerLabel: UILabel!
#IBOutlet weak var longTimerLabel: UILabel!
var seconds = 60 //This variable will hold a starting value of seconds. It could be any amount above 0.
var timer = Timer()
var isTimerRunning = false //This will be used to make sure only one timer is created at a time.
#IBAction func longpressed(_ gestureRecognizer: UILongPressGestureRecognizer) {
shortTimerLabel.text = "longPressed"
}
#IBAction func tappedShortTimer(_ gestureRecognizer: UITapGestureRecognizer) {
shortTimerLabel.text = "ShortPressed"
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
func runTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
func updateTimer() {
seconds += 1 //This will decrement(count down)the seconds.
shortTimerLabel.text = "\(seconds)" //This will update the label.
}
}
im trying to create a stopwatch which can be controlled using gestures. short pressed on label for stop/start and long press to reset time.
In your updateTimer() method, the first line should read seconds -= 1 instead (if you want to count down).
Also, you may want to update your updateTimer() method like this:
func updateTimer() {
seconds -= 1
if seconds == 0 {
timer.invalidate()
isTimerRunning = false
}
shortTimerLabel.text = String(describing: seconds)
}
An other issue here is that you added your runTimer() and updateTimer() method to the wrong place. You should not add them inside your viewDidLoad method.
Your final code would look like this:
var seconds = 60
var timer = Timer()
var isTimerRunning = false
#IBAction func longpressed(_ gestureRecognizer: UILongPressGestureRecognizer) {
resetTimer()
}
#IBAction func tappedShortTimer(_ gestureRecognizer: UITapGestureRecognizer) {
stopStartTimer()
}
override func viewDidLoad() {
super.viewDidLoad()
// ...
}
func stopStartTimer() {
if !isTimerRunning {
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
isTimerRunning = true
} else {
timer.invalidate()
isTimerRunning = false
}
}
func updateTimer() {
seconds -= 1
if seconds == 0 {
timer.invalidate()
isTimerRunning = false
}
shortTimerLabel.text = String(describing: seconds)
}
func resetTimer() {
if isTimerRunning {
seconds = 60
timer.invalidate()
isTimerRunning = false
stopStartTimer()
}
}
The selector should be given in the form #selector(ViewController.updateTimer)
You shouldn't declare functions in the viewDidLoad but outside
You only set the timer in the longpressed function
For stoping it is timer.invalidate()

Running a Timer that counts down in background when app isnt the focus? Swift

I want my countdown timer to suspend and then resume when the app leaves / returns to focus, using the time away to calculate how much time should be deducted.
I am using the app delegate file (not sure thats the right location? or if they are meant to be in the view controllers file as functions of their own?)
Issue is im getting a lot of errors such as:
Value of type 'AppDelegate' has no member 'restTimer'
Use of unresolved identifier 'nextFireDate'
Use of unresolved identifier 'selector'
restTimer was declared as a timer in my view controllers file but when i tried these blocks in that file i got an equal number of errors for unresolved identifiers
and using the following 2 code blocks
func applicationWillResignActive(_ application: UIApplication) {
guard let t = self.restTimer else { return }
nextFireDate = t.fireDate
t.invalidate()
and
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")
Timer.scheduledTimerWithTimeInterval(howMuchLonger, target: target!, selector: selector!, userInfo: nil, repeats: false)
}
}
UPDATE: Added full views code due to issue incorporating the answer
import Foundation
import UIKit
class RestController: UIViewController {
#IBOutlet weak var restRemainingCountdownLabel: UILabel!
#IBOutlet weak var setsRemainingCountdownLabel: UILabel!
#IBOutlet weak var numberOfSetsLabel: UILabel!
#IBOutlet weak var numberOfRestLabel: UILabel!
#IBOutlet weak var adjustSetsStepper: UIStepper!
#IBOutlet weak var adjustRestStepper: UIStepper!
var startDate: Date!
let startDateKey = "start.date"
let interval = TimeInterval(20)
var restTimer: Timer!
var restCount = 0
var setCount = 0
var selectedTime = 1
var selectedSets = 1
private let resignDateKey = "resign.date"
#IBAction func endSetPressed(_ sender: Any) {
if (setCount > 0){
setCount -= 1
setsRemainingCountdownLabel.text = String(setCount)
}
handleTimer()
}
#IBAction func setStepperValueChanged(_ sender: UIStepper) {
numberOfSetsLabel.text = Int(sender.value).description
self.setCount = Int(sender.value)
self.selectedSets = setCount
setsRemainingCountdownLabel.text = String(setCount)
}
#IBAction func restStepperValueChanged(_ sender: UIStepper) {
numberOfRestLabel.text = Int(sender.value).description
let timeMinSec = timeFormatted(totalSeconds: Int(sender.value)*60)
restRemainingCountdownLabel.text = timeMinSec
self.selectedTime = Int(sender.value)
restCount = self.selectedTime * 60
}
#IBAction func resetSetsButton(_ sender: Any) {
setCount = Int(adjustSetsStepper.value)
setsRemainingCountdownLabel.text = String(setCount)
}
override func viewDidLoad() {
super.viewDidLoad()
numberOfSetsLabel.text = String(selectedSets)
numberOfRestLabel.text = String(selectedTime)
createTimer(interval: interval)
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc private func willResignActive(notification: Notification) {
print("resigning")
guard restTimer.isValid else {
UserDefaults.standard.removeObject(forKey: startDateKey)
return
}
restTimer.invalidate()
UserDefaults.standard.set(Date(), forKey: startDateKey)
}
#objc private func didBecomeActive(notification: Notification) {
print("resume")
if let startDate = UserDefaults.standard.object(forKey: startDateKey) as? Date {
let elapsed = -startDate.timeIntervalSinceNow
print("elpased time: \(elapsed) remaining time: \(interval - elapsed)")
if elapsed > interval {
timerUp()
} else {
createTimer(interval: interval - elapsed)
}
}
}
private func createTimer (interval: TimeInterval) {
restTimer = Timer.scheduledTimer(withTimeInterval: interval , repeats: false) {[weak self] _ in
self?.timerUp()
}
startDate = Date()
}
private func timerUp() {
print("At least \(interval) seconds has elapsed")
}
func handleSets() {
if (setCount > 0) {
self.restCount = self.selectedTime * 60
}
handleTimer()
}
func handleTimer() {
if (restTimer?.isValid ?? false) {
restTimer?.invalidate()
restTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(RestController.updateTimer), userInfo: nil, repeats: true)
} else {
restTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(RestController.updateTimer), userInfo: nil, repeats: true)
}
}
func updateTimer() {
if (restCount > 0){
restCount -= 1
} else if (restCount == 0){
restTimer?.invalidate()
}
restRemainingCountdownLabel.text = timeFormatted(totalSeconds: restCount)
}
func timeFormatted(totalSeconds: Int) -> String {
let seconds: Int = totalSeconds % 60
let minutes: Int = (totalSeconds / 60) % 60
return String(format: "%02d:%02d", minutes, seconds)
}
I think you cannot rely on the fact that the app will remain in memory while it is in the background. Thus, you should archive all the data you need to recreate the timer at the exact point.
For example, in applicationWillResignActive
UserDefaults.standard.set(value: t.nextFireDate forKey:"NextFireDate")
and in applicationWillEnterForeground
if let fireDate = UserDefaults.standard.object(forKey: "NextFireDate") {
// setup a timer with the correct fire date
}
You don't have to use the AppDelegate for this because it also posts notifications. You can use the AppDelegate if you want. Here is code using notifications:
class ViewController: UIViewController{
private let startDateKey = "start.date"
private let interval = TimeInterval(20)
private var startDate: Date!
private var timer: Timer!
override func viewDidLoad() {
super.viewDidLoad()
createTimer(interval: interval)
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc private func willResignActive(notification: Notification) {
print("resigning")
guard timer.isValid else {
UserDefaults.standard.removeObject(forKey: startDateKey)
return
}
timer.invalidate()
UserDefaults.standard.set(Date(), forKey: startDateKey)
}
#objc private func didBecomeActive(notification: Notification) {
print("resume")
if let startDate = UserDefaults.standard.object(forKey: startDateKey) as? Date {
let elapsed = -startDate.timeIntervalSinceNow
print("elpased time: \(elapsed) remaining time: \(interval - elapsed)")
if elapsed > interval {
timerUp()
} else {
createTimer(interval: interval - elapsed)
}
}
}
private func createTimer (interval: TimeInterval) {
timer = Timer.scheduledTimer(withTimeInterval: interval , repeats: false) {[weak self] _ in
self?.timerUp()
}
startDate = Date()
}
private func timerUp() {
print("At least \(interval) seconds has elapsed")
}
}

Swift : Pause and Resume NSTimer

I know this has been posted before, but I'm new to swift and i would like to find help as per my situation.
So i need to run a timer, and when the stopMusic() is called(somewhere in my app), i need to pause the timer, then when the playMusic() is called, i need to resume the timer.
Here is what i have:
override func viewDidLoad() {
runFirstDropTimer = NSTimer.scheduledTimerWithTimeInterval(38.2, target: self, selector: "runAnimation", userInfo: nil, repeats: false)
}
func stopMusic() {
//Pause Timer
}
func playMusic() {
//Resume Timer
}
Thank you for any help you can provide!!!
Swift 5.
Resumable timer:
class ResumableTimer: NSObject {
private var timer: Timer? = Timer()
private var callback: () -> Void
private var startTime: TimeInterval?
private var elapsedTime: TimeInterval?
// MARK: Init
init(interval: Double, callback: #escaping () -> Void) {
self.callback = callback
self.interval = interval
}
// MARK: API
var isRepeatable: Bool = false
var interval: Double = 0.0
func isPaused() -> Bool {
guard let timer = timer else { return false }
return !timer.isValid
}
func start() {
runTimer(interval: interval)
}
func pause() {
elapsedTime = Date.timeIntervalSinceReferenceDate - (startTime ?? 0.0)
timer?.invalidate()
}
func resume() {
interval -= elapsedTime ?? 0.0
runTimer(interval: interval)
}
func invalidate() {
timer?.invalidate()
}
func reset() {
startTime = Date.timeIntervalSinceReferenceDate
runTimer(interval: interval)
}
// MARK: Private
private func runTimer(interval: Double) {
startTime = Date.timeIntervalSinceReferenceDate
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: isRepeatable) { [weak self] _ in
self?.callback()
}
}
}
Usage:
private var playTimer: ResumableTimer?
playTimer = ResumableTimer(interval: 5.0) { [weak self] in
self?.doSomethingOnTimerFire()
}
playTimer?.start()
Short answer: You can't. Timers can't be paused.
Instead you need to record the current time when you start the timer:
let startTime = NSDate.timeIntervalSinceReferenceDate()
let interval = 38.2
//Start your timer
runFirstDropTimer = NSTimer.scheduledTimerWithTimeInterval(interval,
target: self,
selector: "runAnimation",
userInfo: nil,
repeats: false)
Then, when you want to pause your timer, invalidate it and figure out how much time is left:
runFirstDropTimer.invalidate()
let elapsed = NSDate.timeIntervalSinceReferenceDate() - startTime
let remaining = interval - elapsed
And finally when you want to resume your timer create a new timer with the remaining value.