How to make a timer work with a loop in swift? - ios

I have multiple views each associated with its own timer. However, only one view is running a time each time. I am accessing each view in a for ... in loop and if the previous timer in the previous view has stopped, which I invalidate in the selector method, I want to fire the time for the next view.
the code looks like this:
var timer = NSTimer()
let timeInterval:NSTimeInterval = 1
var views = [TimerView]()
var timeCountDown: NSTimeInterval = 0
override func viewDidLoad() {
//viewDidLoad create the necessary UIView and fill the views array then pass them to a call to startTimer:
startTimer(views)
}
//startTimer function create the countdown timer
func startTimer(timerViews: [TimerView]){
for timerView in timerViews {
if !timer.valid { // a view with its associated timer start only when the previous timer has run its course and is invalidated
//creating a timer to associate with the current view
timer = NSTimer.scheduledTimerWithTimeInterval(timeInterval,
target: self,
selector: "timerDidEnd:",
userInfo: timerView,
repeats: true)
}
}
}
//Callback function
func timerDidEnd(timer: NSTimer){
let timerView = timer.userInfo as! TimerView
timeCountDown = timeCountDown - timeInterval
timerView.timeLabel.text = timeToString(timeCountDown)
timerView.curValue = CGFloat(timeCountDown)
if timeCount <= 0.0 {
resetTimeCount()
}
}
func timeToString(time:NSTimeInterval) -> String {
let minutes = Int(time) / 60
let seconds = time - Double(minutes) * 60
return String(format:"%02i:%02i",minutes,Int(seconds))
}
func resetTimeCount(){
timer.invalidate()
}
With some print debugging for three views I am getting this output: "running the for loop", "calling view #1 by for loop", "calling the timer", "calling view #2 by for loop", "calling view #3 by for loop","countdown starting" ... That is the countdown only start after the for loop has terminated.
the problem I have is that the first timer run while the for loop calls all the views in my view array. How do I get the for loop to wait for the timer to be invalidated before it iterates to the next view?
What I am trying to achieve is this: the first view starts a countdown; when that countdown finished a second appears and run a countdown as well. Then the third view with a new countdown and so on until the last view. But it looks like the for loop in my code finish looping before the first timer associated with the first view in the collection actually starts
thanks
edit: I am wondering if the timer is not running on a different thread than the for loop?

Your code has a logic problem. It looks like you have a single instance variable, timer. Your for loop starts, and on the first pass, presumably timer is not valid. So you overwrite it with a new timer that IS valid. Now on the second pass through the array of timerViews, you check the same shared timer instance variable, but this time (And all subsequent times) timer is valid, so the body of the if statement doesn't fire.
If you really want a separate timer for each view then you will need to store all those timers. One way would be to have an array of timers that goes along with your array timerViews. Or you could create an array of tuples where each element contains a view and it's associated timer.
Stepping back from your problem, though, why do you want a separate timer for each field? Why not have a single timer, and each time it fires, loop through the array of fields and decide what you need to do with each one? You could have an array of structs that contains references to your views plus status information that lets your loop decide what to do with each one.

Related

How to postpone hiding view when user clicks button again?

If user clicks button I hide view after 3 seconds using DispatchQueue.main.asyncAfter. However if user again clicks that button I wan't to postpone hiding activity for next more 3 seconds so previous call of hiding will be ignored.
I have tried following code but it hides view after first 3 seconds.
class MyView: UIView {
private var hideControls: DispatchWorkItem?
func displayControls() {
isHidden = false
hideControls?.cancel()
hideControls = DispatchWorkItem {
print("displayControls: DispatchWorkItem called ")
self.isHidden = true
}
if let hideControls = hideControls {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3), execute: hideControls)
}
}
}
If you want to ignore the previous work you can work with Timer, add the property timer,
var timer : Timer?
and on your method for hidden use this.
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false, block: {[weak self] (_) in
#add the action that you want
self?.isHidden = true
})
Br.
Instead of a dispatch work item or GCD, use a Timer:
The Timer is an instance property and is an Optional. It is initially nil.
The Timer is a one-time timer for 3 seconds, and its action method hides the button.
When the user presses the button, safely invalidate the Timer with self.timer?.invalidate() and replace it with a new scheduled Timer — self.timer=...
If you think about it, you will see that this solves the problem. No matter whether a timer was already running, pressing the button now means "regardless of what I may have said in the past, three seconds from now is when the button should be hidden."
I use this technique all the time. For instance, I have a game where the user is to be penalized for each ten seconds that goes by without making a move. So if the user moves, I replace the timer with a new ten-second timer. If the user doesn't within the (new?) ten-second period, the timer fires and the penalty is applied. (It happens that I then make a new timer, but that wouldn't be the case in your example.)

How to update a UITableViewCell in firstVC when a change occurs in secondVC automatically and in realtime

I have a editviewcontroller(secondVC)(which you access by tapping on the uitableviewcell task) where you get the option of setting a reminder. When you set the reminder; an icon appears in front of the task in the UITableViewCell in the firstVC.Now I want that once the reminder is triggered and a notification is sent the icon from the task is removed in realtime. Currently, the way I have set it; if you visit the editVC after the task has been reminded, i compare the current time to time set by the user and then updated a label which says "Time's up".
I want a similar thing to occur with the appropriate cell in the firstVC.
FIRSTVC:
When the time is up, it tells you that time is up and when you return to the firstVC the bell icon is removed. But I want it to happen in realtime even if you are in the firstVC and you don't have to go to the secondVC and then return to firstVC to get the changes.
In short, i want the bell icon to be removed when a task has been reminded to the user which is set in the secondVC. Thanks!
EditVC:
Code:
The following code is executed in the editVC in viewDidLoad. If the current time is more than the time selected, it changes the label to "Time's Up" and changes the bellicon tintcolor to white for that specific reminder.
guard let selectedDate = editnotes?.sSelectedDate,
var needsToRemind = editnotes?.sReminderState else {
print("nil")
return
}
if selectedDate <= Date() && needsToRemind {
editnotes?.sReminderDate = "Time's up"
editnotes?.belliconcolor = .white
reminderMsg.text = editnotes?.sReminderDate
}
You can use to fire post notification.post notification is used to perform an action without going to particular VC.
Do you know about post notification or Notification center? If yes, then it is easy to implement in your code otherwise you need some R&D on its. First of all, register post notification on first vc then after on secondvc fire this notification which is register on first vc. It's simple. If you can't get it then i will send some code for easily got it.
You can fire when your timer stop. And one more important things that when you fire notification , you must need to pass current stop time. Because this time is used to first vc method which is register. In this method, you can compare your reminder time and your current time which is passed by notification if both are same then you can hide the bell otherwise not. One more thing, please Manage array to accurate the code after then reload table.
One problem with your code that I can see right away is that you will only say "Time's up" in your edit VC's viewDidLoad. What happens if time is up a few seconds (or even a second) after viewDidLoad?
If this were my code, I would have a Timer property on BOTH the editVC and on the parent (or first) view controller. It would look like this:
var timesUpTimer : Timer?
Which you can set up in viewWillAppear:
if let selectedDate = editnotes?.sSelectedDate
{
self.timesUpTimer = Timer(fireAt: date, interval: 0, target: self, selector: #selector(doSomething), userInfo: nil, repeats: false)
RunLoop.main.add(timesUpTimer, forMode: RunLoopMode.commonModes)
}
// in the editVC you can have this:
func doSomething()
{
editnotes?.sReminderDate = "Time's up"
editnotes?.belliconcolor = .white
reminderMsg.text = editnotes?.sReminderDate
}
and you would do the appropriate thing in the doSomething you have in the firstVC.
Also, in viewWillDisappear for each view controller, you need to invalidate and set your Timer property to nil.

How to create array of timers that update each tableviewcell when they are invoked?

Over the last couple of days I have been trying to figure out how I can update a tableViewCell from TableView controller and it's giving me hard time. Here is where I have reached thus far:
Creating a timer
I set the time for the reminder in editVC(which you accessed by tapping on the tableViewCell). There you can set the time and the reminder is made. Every task has sSelectedDate which is the date set by the user.
This is what I think I should do next: Send this sSelectedDate to the firstViewController(where the tableView resides) and use this date to create a timer in the firstVC.
Question: What is the correct way of sending the particular date from the task to firstVC?
timesUpTimer = Timer(fireAt: selectedDate, interval: 0, target: self, selector: #selector(updateTimeLabel), userInfo: ["taskID": EditNotes], repeats: false)
UserInfo Dilemma
The "taskID" I am sending can be a uniqueTaskID I can generate with each task when a reminder is created. EditNotes is the class of NSManagedObject for CoreData.
Question: What's the correct way of sending the uniqueTaskID to the firstViewController?
Selector Method
#objc func updateTimeLabel(_ timer: Timer)
{
let userInfo = timer.userInfo as Dictionary<String, AnyObject>
var task = userinfo["taskID"] as EditNotes
task.belliconcolor = .white
// do stuff with task object
}
Since I am changing the coreData here I hope FetchedResultsController method is triggered and the tableView is updated.
This is the method invoked when a timer is up.
Question: Should I only rely on FRC or should I actually update the corresponding row in tableview? If yes then how?
Should I create Array for timers
This timer I create in the firstVC is for just one task? Do I have to create an array of timers and save it in coreData also?
Question: How to do it?
TLDR:
How to create timers/array of timers for every element/task of the tableview that when invoked updates the corresponding row in the tableview?

How to restart or stop a Timer in Swift without destroying it?

I'm developing an iOS application using a bluetooth device with a button that is communicating with the iPad. Basically I want a help request to be issued when the button is held for 3 seconds or longer.
From all the documentation I found, I couldn't find a way to stop a Timer without invalidating it, with the invalidate() method. From Apple's documentation:
The run loop then removes the timer (and the strong reference it had to the timer), either just before the
invalidate()
method returns or at some later point. Once invalidated, timer objects cannot be reused.
So the idea in my code is that when the button is pressed, the boolean buttonWasHeld is set to true and a timer is fired. If the button is released, buttonWasHeld is set to false and, when the timer calls the handler it knows the button wasn't held long enough. Then if the button is pressed again within the 3 seconds, the timer is set over again.
Problem is: every button press makes a new timer, which means that repeatedly pressing the button will also issue the help request. Furthermore, all those timers are addressed by the same variable so I can't tell them apart.
Is there a way to uniquely tell what was the last timer created? Or an obscure way to pause/stop it?
Here's the piece of code controlling this feature:
var buttonTimer: Timer?
var buttonWasHeld: Bool = false
func didUpdateModule() {
// gpioListener takes a handler to be called whenever a button is
// pressed or released. isPushed is a self-explanatory boolean.
self.controller.gpioListener() { isPushed in
if isPushed {
self.buttonWasHeld = true
self.buttonTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
if self.buttonWasHeld {
// Issue a help request
self.delegate?.notifyDevice(message: .HELP)
print("Asking for help")
}
}
print("Button was pressed")
}
else {
self.buttonWasHeld = false
// Also tried "self.buttonTimer = nil" here. Didn't work
print("Button was released")
}
}
}
As usual, the answer was quite simple.
If the Timer is declared as a weak var, and not just var, only the weak instantiation will be invalidated. So the code should be:
weak var buttonTimer: Timer?
var buttonWasHeld: Bool = false
func didUpdateModule () {
(...)
else {
// This will only invalidate the current running timer,
// not the whole variable :)
self.buttonTimer.invalidate
// I removed buttonWasHeld, it's not necessary anymore ;)
print("Button was released")
}
}
}

Swift: NSTimer automatically ends without counting down

Swift newbie here. I have two questions:
First, how do I create a timer which AUTOMATICALLY counts down when a ViewController scene is opened? My problem is that the NSTimer ENDS automatically when the scene is opened.
For instance, whenever I go to the said scene, the TimerLabel says: “Time’s Up!”
Before my second question, below is my tweaked code from: makeapppie.com/…/swift-swift-using-nstimer-to-make-a-timer…/
var timer = NSTimer()
let timeInterval:NSTimeInterval = 0.05
let timerEnd:NSTimeInterval = 120.0 //Timer should end in 120 seconds
var timeCount:NSTimeInterval = 0.0
func timerDidEnd(timer:NSTimer){
timeCount = timeCount - timeInterval
if timeCount <= 0 {
TimerLabel.text = "Time's Up!"
timer.invalidate()
}
}
func timeString(time:NSTimeInterval) -> String {
let minutes = Int(time) / 60
let seconds = time - Double(minutes) * 60
return String(format:"%02i:%02i",minutes,Int(seconds))
}
func StartTimer() { // Function called in viewDidLoad
if !timer.valid{
TimerLabel.text = timeString(timeCount)
timer = NSTimer.scheduledTimerWithTimeInterval(timeInterval,
target: self,
selector: "timerDidEnd:",
userInfo: nil,
repeats: true)
}
}
My second question is this: I do not want to use any UISwitch to choose whether to have the timer counting up, or counting down. I need a COUNTING DOWN timer only which AUTOMATICALLY counts down when view is opened. How do I do that?
Please take note that my time format is: “minutes:seconds” as described in the timeString function.
Please help.
Welcome to SO. I think you're in over your head here.
A timer doesn't count up or count down. It fires on a regular interval. You write a method that gets called each time the timer fires.
Note that 0.05 is a pretty short interval for a timer. Timers are not super-accurate, so you might be trying to get too much out of them.
Your code is a confused mess. You start timeCount out at 0. Your timer method, that gets called repeatedly, is called timerDidEnd even though you want your timer to keep running. Your timerDidEnd method subtracts "timeInterval" (.05 seconds) from timeCount when the timer fires, which makes timeCount negative. You then check to see if timeCount is less than 0 (it will be the very first time through this code) and if it is, you invalidate the timer. You also never do anything to display the value of timeCount.
If you want code that counts down, you need to start out with a value that's larger than 0, and subtract a small amount from it each time the timer fires. Then you need to do something with that timeCount value. If you're not displaying it somewhere, it really isn't doing anything useful.
P.S. On an unrelated subject, the avatar pictures on SO are square. You should post a square avatar picture (or create a square crop to post.) When you post a rectangular picture it gets really stretched-out and looks bad.
My problem is that the NSTimer ENDS automatically when the scene is opened.
It sounds like your putting your NSTimer and the code that manages it in a view controller. In that case, it's not surprising that the timer goes away when that view controller does. If the timer should persist from one view controller to the next, then you should manage it from an entirely separate object from the view controller. Since you don't want the timer object to have to keep track of all the view controllers that could possibly be interested in it, have it post notifications that interested view controllers can listen to.

Resources