I'm trying to segue to another view controller once a counter connected to a NSTimer reaches 0.
Heres my timer code:
var counter = 15
var timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "timerAction", userInfo: nil, repeats: true)
func timerAction() {
--counter
timerLabel.text = "\(counter)"
Assuming you've got a segue set up in the storyboard with identifier mySegueID, use this code:
private var timer:NSTimer! //Make sure this is a property of your class
func timerAction() {
--counter
timerLabel.text = "\(counter)"
if (counter == 0) {
timer.invalidate()
timer = nil
performSegueWithIdentifier("mySegueID", sender: nil)
}
}
This will check if the counter is equal to zero. When it is, it'll stop your NSTimer from firing and call the segue that you've set up in your storyboard.
To trigger the segue, just call the appropriate method when the timer executes.
To be more specific, your timer method could be rewritten. It seems you just want to wait for 15 seconds and then fire the segue, but your code does this by decrementing a counter once a second; a better way is to fire the method after the delay; which you can do more elegantly using GCD:
let delay = dispatch_time(DISPATCH_TIME_NOW, 15 * Int64(NSEC_PER_SEC))
dispatch_after(delay, dispatch_get_main_queue()) { () -> Void in
performSegueWithIdentifier("mySegueID", sender: nil)
}
Related
I have created a Timer object to repeatedly execute some code for every second in one of the view controllers in my App. My question is will the system automatically invalidate the timer when I pop the view controller off the navigation stack? Somehow my intuition tells me that it doesn't because the timer object itself is not directly tied to the view controller object.
Edit Note: Below is the code for the VC swift file where the timer is created. Please don't bash me for my amateur code. So basically a VC of this type is created and gets pushed onto the navigation stack. Assuming a scenario where the user didn't press the pause button (in which case the timer is invalidated) before going back to the root view by pressing the back button on the navigation bar, will the timer object gets destroyed?
//
// TimerViewController.swift
// SwiftyTimer
//
// Created by Jiaming Zhou on 5/6/20.
// Copyright © 2020 Jiaming Zhou. All rights reserved.
//
import UIKit
class TimerViewController: UIViewController {
#IBOutlet var countDownLabel: UILabel!
#IBOutlet var imageView: UIImageView!
private var timer: Timer?
private var timePassed = -1
private enum status {
case ongoing
case paused
case completed
}
private enum buttonImage {
case cancelButton
case pauseButton
case resumeButton
}
private var state = status.ongoing
var activity: Activity?
override func viewDidLoad() {
super.viewDidLoad()
if let activity = activity {
imageView.image = UIImage(named: activity.name)
view.backgroundColor = UIColor(named: activity.color)
}
//Start a timer that increments every second
updateTimer()
creatTimer()
}
#IBAction func buttonsPressed(_ sender: UIButton) {
switch sender.tag {
case 0:
timePassed = -1
timer?.invalidate()
state = status.ongoing
creatTimer()
updateTimer()
case 1:
if state == status.ongoing {
timer?.invalidate()
timer = nil
state = status.paused
sender.setBackgroundImage(UIImage(named: "\(buttonImage.resumeButton)"), for: .normal)
sender.setTitle("Resume", for: .normal)
} else if state == status.paused {
creatTimer()
state = status.ongoing
sender.setBackgroundImage(UIImage(named: "\(buttonImage.pauseButton)"), for: .normal)
sender.setTitle("Pause", for: .normal)
}
default:
return
}
}
}
//MARK: - Timer
extension TimerViewController {
func creatTimer() {
let timer = Timer(timeInterval: 1, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: .common)
self.timer = timer
}
#objc func updateTimer() {
if let activity = activity {
timePassed += 1
if timePassed == activity.duration {
self.timer?.invalidate()
state = status.completed
let alert = UIAlertController(title: "Time's Up!", message: "you have completed your activity", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "dismiss", style: .cancel, handler: nil))
present(alert, animated: true)
}
let currentTime = activity.duration - timePassed
let hours = currentTime / 3600
let minutes = (currentTime / 60) % 60
let seconds = currentTime % 60
var countDown = ""
if hours > 0 {
countDown += "\(hours):"
}
if minutes > 9 {
countDown += "\(minutes):"
} else {
countDown += "0\(minutes):"
}
if seconds > 9 {
countDown += "\(seconds)"
} else {
countDown += "0\(seconds)"
}
countDownLabel.text = countDown
}
}
}
The Timer is not automatically invalidated because when you schedule it, the run loop is keeping a strong reference to it, regardless of whether the view controller has been dismissed or not. There are many ways to solve this, but two contemporary solutions include:
Use completion block Timer:
Use [weak self] pattern, so the timer won’t keep strong reference to self, thus breaking the strong reference cycle.
Have deinit method invalidate the timer when the view controller is deallocated.
For example:
class ViewController: UIViewController {
weak var timer: Timer?
override viewDidLoad() {
super.viewDidLoad()
createTimer()
}
deinit {
timer?.invalidate()
}
func createTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
self?.handleTimer(timer)
}
}
func handleTimer(_ timer: Timer) { ... }
}
Note, the weak declaration of the timer variable is unrelated to the breaking of the strong reference cycle, but rather serves another purpose, namely to ensure that when the timer is invalidated (should you invalidate it elsewhere), that the timer variable will automatically be set to nil. The key to breaking the strong reference cycle is the [weak self] in the timer’s closure.
The other approach is to use a GCD timer, which will be canceled when you remove your strong reference to it:
Again, use [weak self] pattern for the closure to avoid strong reference cycle.
But unlike Timer, a GCD timer does automatically stop when the DispatchSourceTimer reference is removed. So no deinit method to stop the dispatch timer is needed.
Thus:
class ViewController: UIViewController {
private let timer = DispatchSource.makeTimerSource(queue: .main)
override viewDidLoad() {
super.viewDidLoad()
configureTimer()
}
func configureTimer() {
timer.setEventHandler { [weak self] in
self?.handleTimer()
}
timer.schedule(deadline: .now(), repeating: 1)
timer.resume()
}
func handleTimer() { ... }
}
I’d generally use the Timer approach, but include the GCD DispatchSourceTimer for the sake of completeness.
The system won't automatically invalidate the timer. The view controller has no relation to the timer itself since the timer gets referenced (strongly) by the RunLoop object.
Apple's documentation also explicitly says that the only way to invalidate a timer is to actually call the method:
https://developer.apple.com/documentation/foundation/timer/1415405-invalidate
This method is the only way to remove a timer from an RunLoop object.
The RunLoop object removes its strong reference to the timer, either
just before the invalidate() method returns or at some later point.
If it was configured with target and user info objects, the receiver
removes its strong references to those objects as well.
I want to create a timer, which goes on while i switch to an another ViewController and come back later. My solution is this way:
I start the timer with an button
then i hand over the integer to the next ViewController
I start the timer with the new integer
So my problem results in step three: I want to start the timer in the function viewDidLoad(). But the timer doesn't starts in the next ViewController.
i hope anybody can help me. Tell me, if there is a better way to do the things i want.
Here is my Code:
var timer = Timer()
var eighthours: Int = 8
var activejob: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
identify_activejob()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(Jobs.jobtime), userInfo: nil, repeats: true)
}
//functions
#objc func jobtime() {
eighthours -= 1
}
I can show you one way. But may as suggested, it has native design flaw and use it depends on the accuracy.
let timer = Timer.init(timeInterval: 1, repeats: true) { (timer) in
let string = ISO8601DateFormatter().string(from: Date())
print("running" + string)
}
}
class TimerViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
RunLoop.main.add(timer, forMode: RunLoop.Mode.default)
// Do any additional setup after loading the view.
}
}
It will keep running until invalidate.
A couple of thoughts:
The eighthours -= 1 in the timer handler is slightly problematic, because it’s presuming that the Timer will fire, without interruption, at the desired timeInterval. But you should accommodate for interruptions in the Timer (e.g. the UI is blocked momentarily for some reason, the user completely leaves the app and returns, etc.).
We often shift from “decrement some counter with every call of the timer handler” to “figure out at what time we want the timer to expire”. This decouples the “model” (the stop time) from the “view” (the frequency with which the UI is updated). By doing this, if you later decide to update your UI with greater frequency (e.g showing milliseconds rather than seconds, probably using CADisplayLink instead of Timer), it doesn’t change the model driving the app. And it makes your app invulnerable to momentary interruptions that might affect the timer.
If you adopt this pattern, then you can pass around this “stop time”, your model, from view controller to view controller, and each view controller can start and stop its own timer as required by the desired UX for that scene.
So, to start a timer that will stop in 8 seconds:
var stopTime: Date? // set this when you start the timer
func startTimer() {
stopTime = Date().addingTimeInterval(8)
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(jobtime(_:)), userInfo: nil, repeats: true)
}
And the timer handler can determine how much time is left with timeIntervalSince to calculate the floating point difference, in seconds, between two dates.
#objc func jobtime(_ timer: Timer) {
let now = Date()
guard let stopTime = stopTime, now < stopTime else {
timer.invalidate()
return
}
let timeRemaining = stopTime.timeIntervalSince(now)
...
}
I also updated jobtime with a timer parameter so that you can see at a glance that it’s a Timer handler.
FYI, your code introduces a strong reference to the view controller that will prevent it from ever being released. This selector-based Timer keeps a strong reference to its target and the run loop keeps a reference to the Timer, so your view controller won’t be released until you invalidate the Timer.
There are a couple of solutions to this:
Start and stop timers as views appear and disappear:
weak var timer: Timer?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(jobtime(_:)), userInfo: nil, repeats: true)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
timer?.invalidate()
}
#objc func jobtime(_ timer: Timer) { ... }
Note, we’re doing this in viewDidAppear and viewDidDisappear (rather than viewDidLoad) to ensure that the starting and stopping of timers is always balanced.
The other pattern is to use block-based Timer, use [weak self] reference to avoid having timer keeping strong reference to the view controller, and then you can invalidate it in the deinit method:
weak var timer: Timer?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
self?.jobtime(timer)
}
}
deinit {
timer?.invalidate()
}
Finally, if you want to update the UI with the greatest possible frequency (e.g. to show milliseconds), you’d probably use a CADisplayLink which is a special timer, perfectly timed for UI updates:
private weak var displayLink: CADisplayLink?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let displayLink = CADisplayLink(target: self, selector: #selector(jobtime(_:)))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
displayLink?.invalidate()
}
#objc func jobtime(_ displayLink: CADisplayLink) { ... }
But the common feature in all of these approaches is that (a) we eliminate strong references from persisting that will interfere with the view controller from getting deallocated when appropriate; and (b) we let every view controller update its UI with whatever frequency it wants.
I'm using nstimer to run code that changes text every 60 seconds.
I was wondering if anyone knew of a way to skip an iteration of the 60 seconds. Say a user doesn't want to read the text currently on display, at the moment they still have to wait 60 seconds.
Is there a way to immediately move on to the next 60 second loop at a button tap?
This is what I have so far:
var array : String[]()
var x = 0
#IBAction func playBtnPressed(sender: UIButton)
{
update()
timer = NSTimer.scheduledTimerWithTimeInterval(60, target: self, selector: #selector(PlayVC.update), userInfo: nil, repeats: true)
}
func update()
{
if x < array.count {
let item = array[x]
aLbl.text = array.itemTitle
x += 1
}
}
#IBAction func skipBtnPressed(sender: UIButton)
{
}
Thanks in advance for any guidance you can give me! :)
In your skipBtnPressed() you should first invalidate the timer, update data and then set another timer. Like this
#IBAction func skipBtnPressed(sender: UIButton)
{
timer.invalidate()
update()
timer = NSTimer.scheduledTimerWithTimeInterval(60, target: self, selector: #selector(PlayVC.update), userInfo: nil, repeats: true)
}
I create and start a self-repeating timer in one of my ViewControllers (let's call it VC1) to have some kind of slideshow of images. When transitioning to any other VC the timer of VC1 appears to keep running as its selector method prints stuff every two seconds. Since this interferes with the timer when returning to VC1 from any other VC I have to remove it at some point.
This is what happens in the console: (runImages() is the timer's selector, the number is the image that should be displayed, as you see its weird...)
I thought the timer would stop once I exit VC1 since I do not save it anywhere. Since this is not the case I thought I might remove the timer when leaving VC1. Is there a method that is being called when VC1 is about to be dismissed?
Another approach I had in mind was removing any timers at the beginning of source code of the other VCs. So, when I enter VC2 I want to check for any timers that are running in the project. Is there a way to do that without making the timer a global variable accessible to all VCs?
Code Reference
This is how I create the timer: (outside a method)
var timer: NSTimer!
Then, in a method I set it:
timer = NSTimer.scheduledTimerWithTimeInterval(2, target: self, selector: "runImages:", userInfo: nil, repeats: true)
runImage() then increases i and calls changeImage() which transitions my imageView's image to the image named like i.
Thanks in advance :)
Update
I made the timer a global variable that is accessible to every VC. The app starts up in VC1, then I transitioned to VC2. There, I inserted this code: if let t = timer {t.invalidate()} and if timer.valid {timer.invalidate()}. Now that made not difference, the timer's selector method keeps printing stuff...
you should keep a reference to the timer in the viewcontroller that is "using" it...
class ViewController: UIViewController {
var timer: NSTimer?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector("timerFired"), userInfo: nil, repeats: true)
}
func timerFired() {
// do whatever you want
}
override func viewWillDisappear(animated: Bool) {
if timer != nil {
timer?.invalidate()
timer = nil
}
super.viewWillDisappear(animated)
}
}
Try this
import UIKit
class ViewController: UIViewController {
let timeInterval: NSTimeInterval = 5 // seconds
// The run loop maintains a strong reference already.
weak var timer: NSTimer?
func startTimer() {
// 1. invalidate previous timer if necessary
timer?.invalidate()
// 2. setup a new timer
timer = NSTimer.scheduledTimerWithTimeInterval(
timeInterval,
target: self,
selector: "timerFired",
userInfo: nil,
repeats: true
)
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
// Need to include #objc marker here
// Function must not be private.
#objc internal func timerFired() {
/*
Perform any UI Updates or whatever...
*/
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
startTimer()
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
stopTimer()
}
}
I've implemented a count-down timer that will automatically start my application if the user doesn't select any options. When the timer hits zero, I invalidate it and fire performSegueWithIdentifier, which segues me to my desired view.
At that point all is fine... well, sort of. I do notice that my view fires twice, but its fine after that. At this point, if I navigate away from that view, then back again, my segue fires and the view loads over and over until I stop my app.
my output window shows:
2015-05-13 21:20:26.880 Web App Browser[43407:7957566] Unbalanced
calls to begin/end appearance transitions for
. 2015-05-13
21:20:28.825 Web App Browser[43407:7957566] Unbalanced calls to
begin/end appearance transitions for .
Here's my view controller:
class StartViewController: UIViewController {
var countDown = Bool()
var timer = NSTimer()
var count = 5
#IBOutlet weak var countdownLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
countDown = AppDelegate().userDefaults.valueForKey("Auto Start") as! Bool
if countDown == true {
var timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("update"), userInfo: nil, repeats: true)
} else {
countdownLabel.text = ""
}
}
func update() {
countdownLabel.text = "\(count)"
if count == 0 {
timer.invalidate()
self.performSegueWithIdentifier("toWeb", sender: nil)
} else {
count--
}
}
}
my storyboard:
In the image below, you see my selected segue, which takes the user from the start screen into a navigation controller that has an embedded viewController. You'll note that I've added my Identifier as "toWeb".
My Question:
What would cause my segue to infinitely loop?
Not sure if this is directly related to your issue, but you are declaring timer twice, once locally and once at class scope.
var countDown = Bool()
var timer = NSTimer()
var count = 5
#IBOutlet weak var countdownLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
countDown = AppDelegate().userDefaults.valueForKey("Auto Start") as! Bool
if countDown == true {
var timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("update"), userInfo: nil, repeats: true)
} else {
countdownLabel.text = ""
}
}
you see the var timer = NSTimer() creates a timer at class scope
var timer = NSTimer.scheduleTimerWithTimeInterval... creates a new timer in the scope of viewDidLoad. I assume that should just be timer = NSTimer.scheduleTimer...
I suppose this was pretty obvious, but my update was getting called every second... because i told it to. And I put my performSegueWithIdentifier inside it. So, easy fix.
var segueFlag = false
func update() {
countdownLabel.text = "\(count)"
if count == 0 {
timer.invalidate()
if segueFlag == false {
self.performSegueWithIdentifier("toWeb", sender: nil)
segueFlag = true
}
} else {
count--
}
}